github.com/grafana/pyroscope@v1.18.0/pkg/test/integration/ingest_pprof_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"testing"
     7  	"time"
     8  
     9  	"connectrpc.com/connect"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    14  	pprof2 "github.com/grafana/pyroscope/pkg/og/convert/pprof"
    15  	"github.com/grafana/pyroscope/pkg/og/convert/pprof/bench"
    16  	"github.com/grafana/pyroscope/pkg/og/convert/pprof/strprofile"
    17  	"github.com/grafana/pyroscope/pkg/og/ingestion"
    18  	"github.com/grafana/pyroscope/pkg/pprof"
    19  	"github.com/grafana/pyroscope/pkg/pprof/testhelper"
    20  )
    21  
    22  const repoRoot = "../../../"
    23  
    24  var (
    25  	golangHeap = []expectedMetric{
    26  		{"memory:alloc_objects:count:space:bytes", nil, 0},
    27  		{"memory:alloc_space:bytes:space:bytes", nil, 1},
    28  		{"memory:inuse_objects:count:space:bytes", nil, 2},
    29  		{"memory:inuse_space:bytes:space:bytes", nil, 3},
    30  	}
    31  	golangCPU = []expectedMetric{
    32  		{"process_cpu:samples:count:cpu:nanoseconds", nil, 0},
    33  		{"process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil, 1},
    34  	}
    35  	_        = golangHeap
    36  	_        = golangCPU
    37  	testdata = []pprofTestData{
    38  		{
    39  			profile:            repoRoot + "pkg/pprof/testdata/heap",
    40  			expectStatusIngest: 200,
    41  			expectStatusPush:   200,
    42  			metrics:            golangHeap,
    43  			needsGoHeapFix:     true,
    44  		},
    45  		{
    46  			profile:            repoRoot + "pkg/pprof/testdata/heap_delta",
    47  			expectStatusPush:   200,
    48  			expectStatusIngest: 200,
    49  			metrics:            golangHeap,
    50  			needsGoHeapFix:     true,
    51  		},
    52  		{
    53  			profile:            repoRoot + "pkg/pprof/testdata/profile_java",
    54  			expectStatusIngest: 200,
    55  			expectStatusPush:   200,
    56  			metrics: []expectedMetric{
    57  				{"process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil, 0},
    58  			},
    59  		},
    60  		{
    61  			profile:            repoRoot + "pkg/pprof/testdata/go.cpu.labels.pprof",
    62  			expectStatusIngest: 200,
    63  			expectStatusPush:   200,
    64  			metrics:            golangCPU,
    65  		},
    66  		{
    67  			profile:            repoRoot + "pkg/og/convert/testdata/cpu.pprof",
    68  			expectStatusIngest: 200,
    69  			expectStatusPush:   200,
    70  
    71  			metrics: golangCPU,
    72  		},
    73  		{
    74  			profile:            repoRoot + "pkg/og/convert/testdata/cpu.pprof",
    75  			prevProfile:        repoRoot + "pkg/og/convert/testdata/cpu.pprof",
    76  			expectStatusIngest: 422,
    77  		},
    78  
    79  		{
    80  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/cpu.pb.gz",
    81  			prevProfile:        "",
    82  			expectStatusIngest: 200,
    83  			expectStatusPush:   200,
    84  			metrics:            golangCPU,
    85  		},
    86  		{
    87  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/cpu-exemplars.pb.gz",
    88  			expectStatusIngest: 200,
    89  			expectStatusPush:   200,
    90  			metrics:            golangCPU,
    91  		},
    92  		{
    93  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/cpu-js.pb.gz",
    94  			expectStatusIngest: 200,
    95  			expectStatusPush:   200,
    96  			metrics: []expectedMetric{
    97  				{"wall:sample:count:wall:microseconds", nil, 0},
    98  				{"wall:wall:microseconds:wall:microseconds", nil, 1},
    99  			},
   100  		},
   101  		{
   102  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/heap.pb",
   103  			expectStatusIngest: 200,
   104  			expectStatusPush:   200,
   105  			metrics:            golangHeap,
   106  			needsGoHeapFix:     true,
   107  		},
   108  		{
   109  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/heap.pb.gz",
   110  			expectStatusIngest: 200,
   111  			expectStatusPush:   200,
   112  			metrics:            golangHeap,
   113  			needsGoHeapFix:     true,
   114  		},
   115  		{
   116  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/heap-js.pprof",
   117  			expectStatusIngest: 200,
   118  			expectStatusPush:   200,
   119  			metrics: []expectedMetric{
   120  				{"memory:space:bytes:space:bytes", nil, 1},
   121  				{"memory:objects:count:space:bytes", nil, 0},
   122  			},
   123  		},
   124  		{
   125  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/nodejs-heap.pb.gz",
   126  			expectStatusIngest: 200,
   127  			expectStatusPush:   200,
   128  			metrics: []expectedMetric{
   129  				{"memory:inuse_space:bytes:inuse_space:bytes", nil, 1},
   130  				{"memory:inuse_objects:count:inuse_space:bytes", nil, 0},
   131  			},
   132  		},
   133  		{
   134  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/nodejs-wall.pb.gz",
   135  			expectStatusIngest: 200,
   136  			expectStatusPush:   200,
   137  			metrics: []expectedMetric{
   138  				{"wall:samples:count:wall:microseconds", nil, 0},
   139  				{"wall:wall:microseconds:wall:microseconds", nil, 1},
   140  			},
   141  		},
   142  		{
   143  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/req_2.pprof",
   144  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/req_2.st.json",
   145  			expectStatusIngest: 200,
   146  			expectStatusPush:   200,
   147  			metrics: []expectedMetric{
   148  				{"goroutines:goroutine:count:goroutine:count", nil, 0},
   149  			},
   150  		},
   151  		{
   152  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/req_3.pprof",
   153  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/req_3.st.json",
   154  			expectStatusIngest: 200,
   155  			expectStatusPush:   200,
   156  			metrics: []expectedMetric{
   157  				{"block:delay:nanoseconds:contentions:count", nil, 1},
   158  				{"block:contentions:count:contentions:count", nil, 0},
   159  			},
   160  		},
   161  		{
   162  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/req_4.pprof",
   163  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/req_4.st.json",
   164  			expectStatusIngest: 200,
   165  			expectStatusPush:   200,
   166  			metrics: []expectedMetric{
   167  				{"mutex:contentions:count:contentions:count", nil, 0},
   168  				{"mutex:delay:nanoseconds:contentions:count", nil, 1},
   169  			},
   170  		},
   171  		{
   172  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/req_5.pprof",
   173  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/req_5.st.json",
   174  			expectStatusIngest: 200,
   175  			expectStatusPush:   200,
   176  			metrics: []expectedMetric{
   177  				{"memory:alloc_objects:count:space:bytes", nil, 0},
   178  				{"memory:alloc_space:bytes:space:bytes", nil, 1},
   179  			},
   180  		},
   181  		{
   182  			// this one have milliseconds in Profile.TimeNanos
   183  			// https://github.com/grafana/pyroscope/pull/2376/files
   184  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/pyspy-1.pb.gz",
   185  			expectStatusIngest: 200,
   186  			expectStatusPush:   200,
   187  			metrics: []expectedMetric{
   188  				{"process_cpu:samples:count::milliseconds", nil, 0},
   189  			},
   190  			spyName: pprof2.SpyNameForFunctionNameRewrite(),
   191  		},
   192  		{
   193  			// this one is broken dotnet pprof
   194  			// it has function.id == 0 for every function
   195  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-3.pb.gz",
   196  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-3.st.json",
   197  			expectStatusIngest: 200,
   198  			expectStatusPush:   400,
   199  			expectedError:      "function id is 0",
   200  			metrics: []expectedMetric{
   201  				{"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0},
   202  			},
   203  			needFunctionIDFix: true,
   204  			spyName:           "dotnetspy",
   205  		},
   206  		{
   207  			// this one is broken dotnet pprof
   208  			// it has function.id == 0 for every function
   209  			// it also has "-" in sample type name
   210  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-73.pb.gz",
   211  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-3.st.json",
   212  			expectStatusIngest: 200,
   213  			expectStatusPush:   400,
   214  			expectedError:      "function id is 0",
   215  			metrics: []expectedMetric{
   216  				// notice how they all use process_cpu metric
   217  				{"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0},
   218  				{"process_cpu:alloc_samples:count::nanoseconds", nil, 2}, // this was rewriten by ingest handler to replace -
   219  				{"process_cpu:alloc_size:bytes::nanoseconds", nil, 3},    // this was rewriten by ingest handler to replace -
   220  			},
   221  			needFunctionIDFix: true,
   222  			spyName:           "dotnetspy",
   223  		},
   224  		{
   225  			// this is a fixed dotnet pprof
   226  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-211.pb.gz",
   227  			sampleTypeConfig:   repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-211.st.json",
   228  			expectStatusIngest: 200,
   229  			expectStatusPush:   200,
   230  			metrics: []expectedMetric{
   231  				{"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0},
   232  				{"process_cpu:alloc_samples:count::nanoseconds", nil, 2},
   233  				{"process_cpu:alloc_size:bytes::nanoseconds", nil, 3},
   234  				{"process_cpu:alloc_size:bytes::nanoseconds", nil, 3},
   235  			},
   236  			spyName: "dotnetspy",
   237  		},
   238  		{
   239  
   240  			profile:            repoRoot + "pkg/og/convert/pprof/testdata/invalid_utf8.pb.gz",
   241  			expectStatusPush:   400,
   242  			expectStatusIngest: 422,
   243  			metrics: []expectedMetric{
   244  				{"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0},
   245  			},
   246  		},
   247  	}
   248  )
   249  
   250  func TestIngest(t *testing.T) {
   251  	EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   252  		for _, td := range testdata {
   253  			t.Run(td.profile, func(t *testing.T) {
   254  				rb := p.NewRequestBuilder(t).
   255  					Spy(td.spyName)
   256  				req := rb.IngestPPROFRequest(td.profile, td.prevProfile, td.sampleTypeConfig)
   257  				p.Ingest(t, req, td.expectStatusIngest)
   258  
   259  				if td.expectStatusIngest == 200 {
   260  					for _, metric := range td.metrics {
   261  						rb.Render(metric.name)
   262  						profile := rb.SelectMergeProfile(metric.name, metric.query)
   263  						assertPPROF(t, profile, metric, td, td.fixAtIngest)
   264  					}
   265  				}
   266  			})
   267  		}
   268  	})
   269  }
   270  
   271  func TestIngestPPROFFixPythonLinenumbers(t *testing.T) {
   272  	EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   273  
   274  		profile := pprof.RawFromProto(&profilev1.Profile{
   275  			SampleType: []*profilev1.ValueType{{
   276  				Type: 5,
   277  				Unit: 6,
   278  			}},
   279  			PeriodType: &profilev1.ValueType{
   280  				Type: 5, Unit: 6,
   281  			},
   282  			StringTable: []string{"", "main", "func1", "func2", "qwe.py", "cpu", "nanoseconds"},
   283  			Period:      1000000000,
   284  			Function: []*profilev1.Function{
   285  				{Id: 1, Name: 1, Filename: 4, SystemName: 1, StartLine: 239},
   286  				{Id: 2, Name: 2, Filename: 4, SystemName: 2, StartLine: 42},
   287  				{Id: 3, Name: 3, Filename: 4, SystemName: 3, StartLine: 7},
   288  			},
   289  			Location: []*profilev1.Location{
   290  				{Id: 1, Line: []*profilev1.Line{{FunctionId: 1, Line: 242}}},
   291  				{Id: 2, Line: []*profilev1.Line{{FunctionId: 2, Line: 50}}},
   292  				{Id: 3, Line: []*profilev1.Line{{FunctionId: 3, Line: 8}}},
   293  			},
   294  			Sample: []*profilev1.Sample{
   295  				{LocationId: []uint64{2, 1}, Value: []int64{10}},
   296  				{LocationId: []uint64{3, 1}, Value: []int64{13}},
   297  			},
   298  		})
   299  
   300  		tempProfileFile, err := os.CreateTemp("", "profile")
   301  		require.NoError(t, err)
   302  		_, err = profile.WriteTo(tempProfileFile)
   303  		assert.NoError(t, err)
   304  		tempProfileFile.Close()
   305  		defer os.Remove(tempProfileFile.Name())
   306  
   307  		rb := p.NewRequestBuilder(t).
   308  			Spy("pyspy")
   309  		req := rb.IngestPPROFRequest(tempProfileFile.Name(), "", "")
   310  		p.Ingest(t, req, 200)
   311  
   312  		renderedProfile := rb.SelectMergeProfile("process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil)
   313  		actual := bench.StackCollapseProto(renderedProfile.Msg, 0, 1)
   314  		expected := []string{
   315  			"qwe.py main;qwe.py func1 10",
   316  			"qwe.py main;qwe.py func2 13",
   317  		}
   318  		assert.Equal(t, expected, actual)
   319  	})
   320  }
   321  
   322  func TestIngestPPROFSanitizeOtelLabels(t *testing.T) {
   323  	EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   324  
   325  		p1 := testhelper.NewProfileBuilder(time.Now().Add(-time.Second).UnixNano()).
   326  			CPUProfile().
   327  			ForStacktraceString("my", "other").
   328  			AddSamples(239)
   329  		p1.Sample[0].Label = []*profilev1.Label{
   330  			{
   331  				Key: p1.AddString("foo.bar"),
   332  				Str: p1.AddString("qwe.asd"),
   333  			},
   334  		}
   335  		p1bs, err := p1.MarshalVT()
   336  		require.NoError(t, err)
   337  
   338  		rb := p.NewRequestBuilder(t)
   339  		rb.Push(rb.PushPPROFRequestFromBytes(p1bs, "process_cpu"), 200, "")
   340  
   341  		renderedProfile := rb.SelectMergeProfile("process_cpu:cpu:nanoseconds:cpu:nanoseconds", map[string]string{
   342  			"foo_bar": "qwe.asd",
   343  		})
   344  		actual, err := strprofile.Stringify(renderedProfile.Msg, strprofile.Options{
   345  			NoTime:     true,
   346  			NoDuration: true,
   347  		})
   348  		require.NoError(t, err)
   349  		expected := `{
   350    "sample_types": [
   351      {
   352        "type": "cpu",
   353        "unit": "nanoseconds"
   354      }
   355    ],
   356    "samples": [
   357      {
   358        "locations": [
   359          {
   360            "address": "0x0",
   361            "lines": [
   362              "my[]@:0"
   363            ],
   364            "mapping": "0x0-0x0@0x0 ()"
   365          },
   366          {
   367            "address": "0x0",
   368            "lines": [
   369              "other[]@:0"
   370            ],
   371            "mapping": "0x0-0x0@0x0 ()"
   372          }
   373        ],
   374        "values": "239"
   375      }
   376    ],
   377    "period": "1000000000"
   378  }`
   379  		assert.JSONEq(t, expected, actual)
   380  	})
   381  }
   382  
   383  func TestGodeltaprofRelabelPush(t *testing.T) {
   384  	const blockSize = 1024
   385  	const metric = "godeltaprof_memory"
   386  
   387  	EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   388  
   389  		p1, _ := testhelper.NewProfileBuilder(time.Now().Add(-time.Second).UnixNano()).
   390  			MemoryProfile().
   391  			ForStacktraceString("my", "other").
   392  			AddSamples(239, 239*blockSize, 1000, 1000*blockSize).
   393  			MarshalVT()
   394  
   395  		p2, _ := testhelper.NewProfileBuilder(time.Now().UnixNano()).
   396  			MemoryProfile().
   397  			ForStacktraceString("my", "other").
   398  			AddSamples(3, 3*blockSize, 1000, 1000*blockSize).
   399  			MarshalVT()
   400  
   401  		rb := p.NewRequestBuilder(t)
   402  		rb.Push(rb.PushPPROFRequestFromBytes(p1, metric), 200, "")
   403  		rb.Push(rb.PushPPROFRequestFromBytes(p2, metric), 200, "")
   404  		renderedProfile := rb.SelectMergeProfile("memory:alloc_objects:count:space:bytes", nil)
   405  		actual := bench.StackCollapseProto(renderedProfile.Msg, 0, 1)
   406  		expected := []string{
   407  			"other;my 242",
   408  		}
   409  		assert.Equal(t, expected, actual)
   410  	})
   411  }
   412  
   413  func TestPushStringTableOOBSampleType(t *testing.T) {
   414  	const blockSize = 1024
   415  	const metric = "godeltaprof_memory"
   416  
   417  	EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   418  
   419  		testdata := []struct {
   420  			name        string
   421  			corrupt     func(p *testhelper.ProfileBuilder)
   422  			expectedErr string
   423  		}{
   424  			{
   425  				name: "sample type",
   426  				corrupt: func(p *testhelper.ProfileBuilder) {
   427  					p.SampleType[0].Type = 100500
   428  				},
   429  				expectedErr: "sample type type string index out of range",
   430  			},
   431  			{
   432  				name: "function name",
   433  				corrupt: func(p *testhelper.ProfileBuilder) {
   434  					p.Function[0].Name = 100500
   435  				},
   436  				expectedErr: "function name string index out of range",
   437  			},
   438  			{
   439  				name: "mapping",
   440  				corrupt: func(p *testhelper.ProfileBuilder) {
   441  					p.Mapping[0].Filename = 100500
   442  				},
   443  				expectedErr: "mapping file name string index out of range",
   444  			},
   445  			{
   446  				name: "Sample label",
   447  				corrupt: func(p *testhelper.ProfileBuilder) {
   448  					p.Sample[0].Label = []*profilev1.Label{{
   449  						Key: 100500,
   450  					}}
   451  				},
   452  				expectedErr: "sample label string index out of range",
   453  			},
   454  			{
   455  				name: "String 0 not empty",
   456  				corrupt: func(p *testhelper.ProfileBuilder) {
   457  					p.StringTable[0] = "hmmm"
   458  				},
   459  				expectedErr: "string 0 should be empty string",
   460  			},
   461  		}
   462  		for _, td := range testdata {
   463  			t.Run(td.name, func(t *testing.T) {
   464  				p1 := testhelper.NewProfileBuilder(time.Now().Add(-time.Second).UnixNano()).
   465  					MemoryProfile().
   466  					ForStacktraceString("my", "other").
   467  					AddSamples(239, 239*blockSize, 1000, 1000*blockSize)
   468  				td.corrupt(p1)
   469  				p1bs, err := p1.MarshalVT()
   470  				require.NoError(t, err)
   471  
   472  				rb := p.NewRequestBuilder(t)
   473  				rb.Push(rb.PushPPROFRequestFromBytes(p1bs, metric), 400, td.expectedErr)
   474  			})
   475  		}
   476  	})
   477  }
   478  
   479  func TestPush(t *testing.T) {
   480  	EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
   481  
   482  		for _, td := range testdata {
   483  			if td.prevProfile != "" {
   484  				continue
   485  			}
   486  			t.Run(td.profile, func(t *testing.T) {
   487  				rb := p.NewRequestBuilder(t)
   488  
   489  				req := rb.PushPPROFRequestFromFile(td.profile, td.metrics[0].name)
   490  				rb.Push(req, td.expectStatusPush, td.expectedError)
   491  
   492  				if td.expectStatusPush == 200 {
   493  					for _, metric := range td.metrics {
   494  						rb.Render(metric.name)
   495  						profile := rb.SelectMergeProfile(metric.name, metric.query)
   496  
   497  						assertPPROF(t, profile, metric, td, td.fixAtPush)
   498  					}
   499  				}
   500  			})
   501  		}
   502  	})
   503  }
   504  
   505  func assertPPROF(t *testing.T, resp *connect.Response[profilev1.Profile], metric expectedMetric, testdatum pprofTestData, fix func(*pprof.Profile) *pprof.Profile) {
   506  
   507  	assert.Equal(t, 1, len(resp.Msg.SampleType))
   508  
   509  	profileBytes, err := os.ReadFile(testdatum.profile)
   510  	require.NoError(t, err)
   511  	expectedProfile, err := pprof.RawFromBytes(profileBytes)
   512  	require.NoError(t, err)
   513  
   514  	if fix != nil {
   515  		expectedProfile = fix(expectedProfile)
   516  	}
   517  
   518  	actualStacktraces := bench.StackCollapseProto(resp.Msg, 0, 1)
   519  	expectedStacktraces := bench.StackCollapseProto(expectedProfile.Profile, metric.valueIDX, 1)
   520  
   521  	for i, valueType := range expectedProfile.SampleType {
   522  		fmt.Println(i, expectedProfile.StringTable[valueType.Type])
   523  	}
   524  	require.Equal(t, expectedStacktraces, actualStacktraces)
   525  }
   526  
   527  type pprofTestData struct {
   528  	profile            string
   529  	prevProfile        string
   530  	sampleTypeConfig   string
   531  	spyName            string
   532  	expectStatusIngest int
   533  	expectStatusPush   int
   534  	expectedError      string
   535  	metrics            []expectedMetric
   536  	needFunctionIDFix  bool
   537  	needsGoHeapFix     bool
   538  }
   539  
   540  func (d *pprofTestData) fixAtPush(p *pprof.Profile) *pprof.Profile {
   541  	if d.needsGoHeapFix {
   542  		p.Profile = pprof.FixGoProfile(p.Profile)
   543  	}
   544  	return p
   545  }
   546  
   547  func (d *pprofTestData) fixAtIngest(p *pprof.Profile) *pprof.Profile {
   548  	if d.spyName == pprof2.SpyNameForFunctionNameRewrite() {
   549  		pprof2.FixFunctionNamesForScriptingLanguages(p, ingestion.Metadata{SpyName: d.spyName})
   550  	}
   551  	if d.needFunctionIDFix {
   552  		pprof2.FixFunctionIDForBrokenDotnet(p.Profile)
   553  	}
   554  	if d.needsGoHeapFix {
   555  		p.Profile = pprof.FixGoProfile(p.Profile)
   556  	}
   557  	return p
   558  }
   559  
   560  type expectedMetric struct {
   561  	name     string
   562  	query    map[string]string
   563  	valueIDX int
   564  }