github.com/grafana/pyroscope@v1.18.0/pkg/model/time_series_builder_test.go (about) 1 package model 2 3 import ( 4 "sort" 5 "testing" 6 7 "github.com/stretchr/testify/assert" 8 "github.com/stretchr/testify/require" 9 10 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 11 schemav1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1" 12 ) 13 14 func TestTimeSeriesBuilder_NoExemplarsForEmptyProfileID(t *testing.T) { 15 builder := NewTimeSeriesBuilder() 16 labels := Labels{ 17 {Name: "service_name", Value: "api"}, 18 {Name: "env", Value: "prod"}, 19 } 20 21 builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "") 22 builder.Add(1, labels, 1000, 200.0, schemav1.Annotations{}, "") 23 24 series := builder.BuildWithExemplars() 25 require.Len(t, series, 1) 26 require.Len(t, series[0].Points, 2) 27 28 for _, point := range series[0].Points { 29 assert.Empty(t, point.Exemplars, "Empty profileID should not create exemplars") 30 } 31 } 32 33 func TestTimeSeriesBuilder_Build_NoExemplars(t *testing.T) { 34 builder := NewTimeSeriesBuilder() 35 labels := Labels{ 36 {Name: "service_name", Value: "api"}, 37 } 38 39 builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-1") 40 41 series := builder.Build() 42 require.Len(t, series, 1) 43 require.Len(t, series[0].Points, 1) 44 assert.Empty(t, series[0].Points[0].Exemplars, "Build() should not attach exemplars") 45 } 46 47 func TestTimeSeriesBuilder_BuildWithExemplars_AttachesExemplars(t *testing.T) { 48 builder := NewTimeSeriesBuilder() 49 labels := Labels{ 50 {Name: "service_name", Value: "api"}, 51 {Name: "pod", Value: "pod-123"}, 52 } 53 54 builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-1") 55 56 series := builder.BuildWithExemplars() 57 require.Len(t, series, 1) 58 require.Len(t, series[0].Points, 1) 59 require.Len(t, series[0].Points[0].Exemplars, 1) 60 61 exemplar := series[0].Points[0].Exemplars[0] 62 assert.Equal(t, "profile-1", exemplar.ProfileId) 63 assert.Equal(t, uint64(100), exemplar.Value) 64 assert.Equal(t, int64(1000), exemplar.Timestamp) 65 66 assert.Len(t, exemplar.Labels, 2) 67 assert.Equal(t, "api", findLabelValue(exemplar.Labels, "service_name")) 68 assert.Equal(t, "pod-123", findLabelValue(exemplar.Labels, "pod")) 69 } 70 71 func TestTimeSeriesBuilder_MultipleExemplarsAtSameTimestamp(t *testing.T) { 72 builder := NewTimeSeriesBuilder() 73 labels := Labels{ 74 {Name: "service_name", Value: "api"}, 75 } 76 77 builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-1") 78 builder.Add(1, labels, 1000, 200.0, schemav1.Annotations{}, "profile-2") 79 builder.Add(1, labels, 1000, 300.0, schemav1.Annotations{}, "profile-3") 80 81 series := builder.BuildWithExemplars() 82 require.Len(t, series, 1) 83 require.Len(t, series[0].Points, 3) 84 85 // All 3 points at timestamp 1000 should have all 3 exemplars 86 for _, point := range series[0].Points { 87 require.Len(t, point.Exemplars, 3) 88 profileIDs := make(map[string]bool) 89 for _, ex := range point.Exemplars { 90 profileIDs[ex.ProfileId] = true 91 } 92 assert.True(t, profileIDs["profile-1"]) 93 assert.True(t, profileIDs["profile-2"]) 94 assert.True(t, profileIDs["profile-3"]) 95 } 96 } 97 98 func TestTimeSeriesBuilder_GroupBy(t *testing.T) { 99 builder := NewTimeSeriesBuilder("service_name") 100 labels1 := Labels{ 101 {Name: "service_name", Value: "api"}, 102 {Name: "pod", Value: "pod-1"}, 103 } 104 labels2 := Labels{ 105 {Name: "service_name", Value: "api"}, 106 {Name: "pod", Value: "pod-2"}, 107 } 108 109 builder.Add(1, labels1, 1000, 100.0, schemav1.Annotations{}, "profile-1") 110 builder.Add(2, labels2, 1000, 200.0, schemav1.Annotations{}, "profile-2") 111 112 series := builder.BuildWithExemplars() 113 114 // Should be grouped into 1 series by service_name 115 require.Len(t, series, 1) 116 assert.Len(t, series[0].Labels, 1) 117 assert.Equal(t, "service_name", series[0].Labels[0].Name) 118 assert.Equal(t, "api", series[0].Labels[0].Value) 119 120 require.Len(t, series[0].Points, 2) 121 122 // Both exemplars should be at timestamp 1000, grouped together 123 point := series[0].Points[0] 124 require.Len(t, point.Exemplars, 2) 125 126 // Exemplars should have only non-grouped labels (pod), not service_name 127 for _, ex := range point.Exemplars { 128 assert.Len(t, ex.Labels, 1) 129 assert.NotEmpty(t, findLabelValue(ex.Labels, "pod")) 130 assert.Empty(t, findLabelValue(ex.Labels, "service_name")) 131 } 132 } 133 134 func TestTimeSeriesBuilder_ExemplarDeduplication(t *testing.T) { 135 builder := NewTimeSeriesBuilder() 136 labels := Labels{ 137 {Name: "service_name", Value: "api"}, 138 {Name: "pod", Value: "pod-1"}, 139 } 140 141 builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-dup") 142 builder.Add(1, labels, 1000, 200.0, schemav1.Annotations{}, "profile-dup") 143 144 series := builder.BuildWithExemplars() 145 require.Len(t, series, 1) 146 require.Len(t, series[0].Points, 2) 147 148 // Should deduplicate to 1 exemplar per point 149 for _, point := range series[0].Points { 150 require.Len(t, point.Exemplars, 1) 151 assert.Equal(t, "profile-dup", point.Exemplars[0].ProfileId) 152 } 153 } 154 155 func TestExemplarBuilder_SameProfileIDDifferentValues(t *testing.T) { 156 builder := NewExemplarBuilder() 157 158 labels1 := Labels{ 159 {Name: "pod", Value: "pod-1"}, 160 } 161 labels2 := Labels{ 162 {Name: "pod", Value: "pod-1"}, 163 {Name: "span_name", Value: "POST"}, 164 } 165 166 builder.Add(1, labels1, 1000, "profile-123", 12830000000) 167 builder.Add(2, labels2, 1000, "profile-123", 110000000) 168 169 exemplars := builder.Build() 170 require.Len(t, exemplars, 1) 171 172 exemplar := exemplars[0] 173 assert.Equal(t, "profile-123", exemplar.ProfileId) 174 assert.Equal(t, int64(1000), exemplar.Timestamp) 175 assert.Equal(t, uint64(12940000000), exemplar.Value) 176 177 // Labels should be intersected 178 assert.Len(t, exemplar.Labels, 1) 179 assert.Equal(t, "pod", exemplar.Labels[0].Name) 180 assert.Equal(t, "pod-1", exemplar.Labels[0].Value) 181 } 182 183 func TestExemplarBuilder_DifferentProfileIDsNotSummed(t *testing.T) { 184 builder := NewExemplarBuilder() 185 186 labels1 := Labels{ 187 {Name: "pod", Value: "pod-1"}, 188 {Name: "span_name", Value: "POST"}, 189 } 190 labels2 := Labels{ 191 {Name: "pod", Value: "pod-2"}, 192 {Name: "span_name", Value: "POST"}, 193 } 194 195 builder.Add(1, labels1, 1000, "profile-abc", 110000000) 196 builder.Add(2, labels2, 1000, "profile-def", 150000000) 197 198 exemplars := builder.Build() 199 require.Len(t, exemplars, 2) 200 201 // Sort by profile ID to ensure consistent ordering 202 sort.Slice(exemplars, func(i, j int) bool { 203 return exemplars[i].ProfileId < exemplars[j].ProfileId 204 }) 205 206 // First exemplar 207 assert.Equal(t, "profile-abc", exemplars[0].ProfileId) 208 assert.Equal(t, uint64(110000000), exemplars[0].Value) 209 210 // Second exemplar 211 assert.Equal(t, "profile-def", exemplars[1].ProfileId) 212 assert.Equal(t, uint64(150000000), exemplars[1].Value) 213 } 214 215 func TestTimeSeriesBuilder_MultipleSeries(t *testing.T) { 216 builder := NewTimeSeriesBuilder("env") 217 labels1 := Labels{ 218 {Name: "service_name", Value: "api"}, 219 {Name: "env", Value: "prod"}, 220 } 221 labels2 := Labels{ 222 {Name: "service_name", Value: "api"}, 223 {Name: "env", Value: "staging"}, 224 } 225 226 builder.Add(1, labels1, 1000, 100.0, schemav1.Annotations{}, "prod-profile") 227 builder.Add(2, labels2, 1000, 200.0, schemav1.Annotations{}, "staging-profile") 228 229 series := builder.BuildWithExemplars() 230 require.Len(t, series, 2) 231 232 seriesByEnv := make(map[string]*typesv1.Series) 233 for _, s := range series { 234 for _, lp := range s.Labels { 235 if lp.Name == "env" { 236 seriesByEnv[lp.Value] = s 237 break 238 } 239 } 240 } 241 242 prodSeries := seriesByEnv["prod"] 243 require.NotNil(t, prodSeries) 244 require.Len(t, prodSeries.Points, 1) 245 require.Len(t, prodSeries.Points[0].Exemplars, 1) 246 assert.Equal(t, "prod-profile", prodSeries.Points[0].Exemplars[0].ProfileId) 247 248 stagingSeries := seriesByEnv["staging"] 249 require.NotNil(t, stagingSeries) 250 require.Len(t, stagingSeries.Points, 1) 251 require.Len(t, stagingSeries.Points[0].Exemplars, 1) 252 assert.Equal(t, "staging-profile", stagingSeries.Points[0].Exemplars[0].ProfileId) 253 } 254 255 func findLabelValue(labels []*typesv1.LabelPair, name string) string { 256 for _, lp := range labels { 257 if lp.Name == name { 258 return lp.Value 259 } 260 } 261 return "" 262 }