github.com/grafana/pyroscope@v1.18.0/pkg/model/recording_rule_test.go (about) 1 package model 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/require" 7 8 settingsv1 "github.com/grafana/pyroscope/api/gen/proto/go/settings/v1" 9 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 10 ) 11 12 func Test_NewRecordingRule_GroupByValidation(t *testing.T) { 13 tests := []struct { 14 name string 15 rule *settingsv1.RecordingRule 16 wantErr string 17 }{ 18 { 19 name: "valid group_by", 20 rule: &settingsv1.RecordingRule{ 21 Id: "test", 22 MetricName: "profiles_recorded_test", 23 Matchers: []string{`{__profile_type__="cpu"}`}, 24 GroupBy: []string{"valid_label", "another_valid"}, 25 ExternalLabels: []*typesv1.LabelPair{ 26 {Name: "external_label", Value: "value"}, 27 }, 28 }, 29 wantErr: "", 30 }, 31 { 32 name: "invalid group_by with dot", 33 rule: &settingsv1.RecordingRule{ 34 Id: "test", 35 MetricName: "profiles_recorded_test", 36 Matchers: []string{`{__profile_type__="cpu"}`}, 37 GroupBy: []string{"service.name"}, 38 ExternalLabels: []*typesv1.LabelPair{ 39 {Name: "external_label", Value: "value"}, 40 }, 41 }, 42 wantErr: `group_by label "service.name" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 43 }, 44 { 45 name: "invalid group_by with UTF-8", 46 rule: &settingsv1.RecordingRule{ 47 Id: "test", 48 MetricName: "profiles_recorded_test", 49 Matchers: []string{`{__profile_type__="cpu"}`}, 50 GroupBy: []string{"世界"}, 51 ExternalLabels: []*typesv1.LabelPair{ 52 {Name: "external_label", Value: "value"}, 53 }, 54 }, 55 wantErr: `group_by label "世界" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 56 }, 57 { 58 name: "invalid group_by starts with number", 59 rule: &settingsv1.RecordingRule{ 60 Id: "test", 61 MetricName: "profiles_recorded_test", 62 Matchers: []string{`{__profile_type__="cpu"}`}, 63 GroupBy: []string{"123invalid"}, 64 ExternalLabels: []*typesv1.LabelPair{ 65 {Name: "external_label", Value: "value"}, 66 }, 67 }, 68 wantErr: `group_by label "123invalid" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 69 }, 70 { 71 name: "invalid group_by with hyphen", 72 rule: &settingsv1.RecordingRule{ 73 Id: "test", 74 MetricName: "profiles_recorded_test", 75 Matchers: []string{`{__profile_type__="cpu"}`}, 76 GroupBy: []string{"service-name"}, 77 ExternalLabels: []*typesv1.LabelPair{ 78 {Name: "external_label", Value: "value"}, 79 }, 80 }, 81 wantErr: `group_by label "service-name" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 82 }, 83 } 84 85 for _, tt := range tests { 86 t.Run(tt.name, func(t *testing.T) { 87 _, err := NewRecordingRule(tt.rule) 88 if tt.wantErr != "" { 89 require.Error(t, err) 90 require.EqualError(t, err, tt.wantErr) 91 } else { 92 require.NoError(t, err) 93 } 94 }) 95 } 96 } 97 98 func Test_NewRecordingRule_ExternalLabelsValidation(t *testing.T) { 99 tests := []struct { 100 name string 101 rule *settingsv1.RecordingRule 102 wantErr string 103 }{ 104 { 105 name: "valid external_labels", 106 rule: &settingsv1.RecordingRule{ 107 Id: "test", 108 MetricName: "profiles_recorded_test", 109 Matchers: []string{`{__profile_type__="cpu"}`}, 110 GroupBy: []string{"valid_label"}, 111 ExternalLabels: []*typesv1.LabelPair{ 112 {Name: "valid_label", Value: "value"}, 113 {Name: "another_valid", Value: "value"}, 114 }, 115 }, 116 wantErr: "", 117 }, 118 { 119 name: "invalid external_labels with dot", 120 rule: &settingsv1.RecordingRule{ 121 Id: "test", 122 MetricName: "profiles_recorded_test", 123 Matchers: []string{`{__profile_type__="cpu"}`}, 124 GroupBy: []string{"valid_label"}, 125 ExternalLabels: []*typesv1.LabelPair{ 126 {Name: "service.name", Value: "foo"}, 127 }, 128 }, 129 wantErr: `external_labels name "service.name" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 130 }, 131 { 132 name: "invalid external_labels with UTF-8", 133 rule: &settingsv1.RecordingRule{ 134 Id: "test", 135 MetricName: "profiles_recorded_test", 136 Matchers: []string{`{__profile_type__="cpu"}`}, 137 GroupBy: []string{"valid_label"}, 138 ExternalLabels: []*typesv1.LabelPair{ 139 {Name: "世界", Value: "value"}, 140 }, 141 }, 142 wantErr: `external_labels name "世界" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 143 }, 144 { 145 name: "invalid external_labels starts with number", 146 rule: &settingsv1.RecordingRule{ 147 Id: "test", 148 MetricName: "profiles_recorded_test", 149 Matchers: []string{`{__profile_type__="cpu"}`}, 150 GroupBy: []string{"valid_label"}, 151 ExternalLabels: []*typesv1.LabelPair{ 152 {Name: "123invalid", Value: "value"}, 153 }, 154 }, 155 wantErr: `external_labels name "123invalid" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 156 }, 157 { 158 name: "invalid external_labels with hyphen", 159 rule: &settingsv1.RecordingRule{ 160 Id: "test", 161 MetricName: "profiles_recorded_test", 162 Matchers: []string{`{__profile_type__="cpu"}`}, 163 GroupBy: []string{"valid_label"}, 164 ExternalLabels: []*typesv1.LabelPair{ 165 {Name: "service-name", Value: "value"}, 166 }, 167 }, 168 wantErr: `external_labels name "service-name" must match ^[a-zA-Z_][a-zA-Z0-9_]*$`, 169 }, 170 { 171 name: "valid external_labels UTF-8 value is allowed", 172 rule: &settingsv1.RecordingRule{ 173 Id: "test", 174 MetricName: "profiles_recorded_test", 175 Matchers: []string{`{__profile_type__="cpu"}`}, 176 GroupBy: []string{"valid_label"}, 177 ExternalLabels: []*typesv1.LabelPair{ 178 {Name: "valid_label", Value: "世界"}, 179 }, 180 }, 181 wantErr: "", 182 }, 183 } 184 185 for _, tt := range tests { 186 t.Run(tt.name, func(t *testing.T) { 187 _, err := NewRecordingRule(tt.rule) 188 if tt.wantErr != "" { 189 require.Error(t, err) 190 require.EqualError(t, err, tt.wantErr) 191 } else { 192 require.NoError(t, err) 193 } 194 }) 195 } 196 } 197 198 func Test_NewRecordingRule_MetricNameValidation(t *testing.T) { 199 tests := []struct { 200 name string 201 rule *settingsv1.RecordingRule 202 wantErr string 203 }{ 204 { 205 name: "valid metric_name", 206 rule: &settingsv1.RecordingRule{ 207 Id: "test", 208 MetricName: "profiles_recorded_test", 209 Matchers: []string{`{__profile_type__="cpu"}`}, 210 GroupBy: []string{"valid_label"}, 211 ExternalLabels: []*typesv1.LabelPair{ 212 {Name: "external_label", Value: "value"}, 213 }, 214 }, 215 wantErr: "", 216 }, 217 { 218 name: "valid metric_name with underscores and numbers", 219 rule: &settingsv1.RecordingRule{ 220 Id: "test", 221 MetricName: "profiles_recorded_test_123", 222 Matchers: []string{`{__profile_type__="cpu"}`}, 223 GroupBy: []string{"valid_label"}, 224 ExternalLabels: []*typesv1.LabelPair{ 225 {Name: "external_label", Value: "value"}, 226 }, 227 }, 228 wantErr: "", 229 }, 230 { 231 name: "invalid metric_name with dot", 232 rule: &settingsv1.RecordingRule{ 233 Id: "test", 234 MetricName: "profiles_recorded.test", 235 Matchers: []string{`{__profile_type__="cpu"}`}, 236 GroupBy: []string{"valid_label"}, 237 ExternalLabels: []*typesv1.LabelPair{ 238 {Name: "external_label", Value: "value"}, 239 }, 240 }, 241 wantErr: `invalid metric name: profiles_recorded.test`, 242 }, 243 { 244 name: "invalid metric_name with UTF-8", 245 rule: &settingsv1.RecordingRule{ 246 Id: "test", 247 MetricName: "profiles_recorded_世界", 248 Matchers: []string{`{__profile_type__="cpu"}`}, 249 GroupBy: []string{"valid_label"}, 250 ExternalLabels: []*typesv1.LabelPair{ 251 {Name: "external_label", Value: "value"}, 252 }, 253 }, 254 wantErr: `invalid metric name: profiles_recorded_世界`, 255 }, 256 { 257 name: "invalid metric_name with emoji", 258 rule: &settingsv1.RecordingRule{ 259 Id: "test", 260 MetricName: "profiles_recorded_test_😘", 261 Matchers: []string{`{__profile_type__="cpu"}`}, 262 GroupBy: []string{"valid_label"}, 263 ExternalLabels: []*typesv1.LabelPair{ 264 {Name: "external_label", Value: "value"}, 265 }, 266 }, 267 wantErr: `invalid metric name: profiles_recorded_test_😘`, 268 }, 269 { 270 name: "invalid metric_name starts with number", 271 rule: &settingsv1.RecordingRule{ 272 Id: "test", 273 MetricName: "123_invalid", 274 Matchers: []string{`{__profile_type__="cpu"}`}, 275 GroupBy: []string{"valid_label"}, 276 ExternalLabels: []*typesv1.LabelPair{ 277 {Name: "external_label", Value: "value"}, 278 }, 279 }, 280 wantErr: `invalid metric name: 123_invalid`, 281 }, 282 { 283 name: "invalid metric_name with hyphen", 284 rule: &settingsv1.RecordingRule{ 285 Id: "test", 286 MetricName: "profiles_recorded-test", 287 Matchers: []string{`{__profile_type__="cpu"}`}, 288 GroupBy: []string{"valid_label"}, 289 ExternalLabels: []*typesv1.LabelPair{ 290 {Name: "external_label", Value: "value"}, 291 }, 292 }, 293 wantErr: `invalid metric name: profiles_recorded-test`, 294 }, 295 { 296 name: "invalid metric_name with exclamation marks", 297 rule: &settingsv1.RecordingRule{ 298 Id: "test", 299 MetricName: "profiles_recorded_test!!!", 300 Matchers: []string{`{__profile_type__="cpu"}`}, 301 GroupBy: []string{"valid_label"}, 302 ExternalLabels: []*typesv1.LabelPair{ 303 {Name: "external_label", Value: "value"}, 304 }, 305 }, 306 wantErr: `invalid metric name: profiles_recorded_test!!!`, 307 }, 308 } 309 310 for _, tt := range tests { 311 t.Run(tt.name, func(t *testing.T) { 312 _, err := NewRecordingRule(tt.rule) 313 if tt.wantErr != "" { 314 require.Error(t, err) 315 require.EqualError(t, err, tt.wantErr) 316 } else { 317 require.NoError(t, err) 318 } 319 }) 320 } 321 } 322 323 func Test_NewRecordingRule_MatchersValidation(t *testing.T) { 324 tests := []struct { 325 name string 326 rule *settingsv1.RecordingRule 327 wantErr string 328 }{ 329 { 330 name: "valid matchers with standard characters", 331 rule: &settingsv1.RecordingRule{ 332 Id: "test", 333 MetricName: "profiles_recorded_test", 334 Matchers: []string{`{__profile_type__="cpu"}`}, 335 GroupBy: []string{"valid_label"}, 336 ExternalLabels: []*typesv1.LabelPair{ 337 {Name: "external_label", Value: "value"}, 338 }, 339 }, 340 wantErr: "", 341 }, 342 { 343 name: "valid matchers with UTF-8 characters", 344 rule: &settingsv1.RecordingRule{ 345 Id: "test", 346 MetricName: "profiles_recorded_test", 347 Matchers: []string{`{service_name="世界", __profile_type__="cpu"}`}, 348 GroupBy: []string{"valid_label"}, 349 ExternalLabels: []*typesv1.LabelPair{ 350 {Name: "external_label", Value: "value"}, 351 }, 352 }, 353 wantErr: "", 354 }, 355 { 356 name: "valid matchers with UTF-8 label names (quoted)", 357 rule: &settingsv1.RecordingRule{ 358 Id: "test", 359 MetricName: "profiles_recorded_test", 360 Matchers: []string{`{"世界"="value", __profile_type__="cpu"}`}, 361 GroupBy: []string{"valid_label"}, 362 ExternalLabels: []*typesv1.LabelPair{ 363 {Name: "external_label", Value: "value"}, 364 }, 365 }, 366 wantErr: "", 367 }, 368 { 369 name: "valid matchers with emojis in label names and values", 370 rule: &settingsv1.RecordingRule{ 371 Id: "test", 372 MetricName: "profiles_recorded_test", 373 Matchers: []string{`{"utf-8 matchers are fine 🔥"="we don't export them", __profile_type__="cpu"}`}, 374 GroupBy: []string{"valid_label"}, 375 ExternalLabels: []*typesv1.LabelPair{ 376 {Name: "external_label", Value: "value"}, 377 }, 378 }, 379 wantErr: "", 380 }, 381 { 382 name: "valid matchers with dots in label names", 383 rule: &settingsv1.RecordingRule{ 384 Id: "test", 385 MetricName: "profiles_recorded_test", 386 Matchers: []string{`{"service.name"="my-service", __profile_type__="cpu"}`}, 387 GroupBy: []string{"valid_label"}, 388 ExternalLabels: []*typesv1.LabelPair{ 389 {Name: "external_label", Value: "value"}, 390 }, 391 }, 392 wantErr: "", 393 }, 394 } 395 396 for _, tt := range tests { 397 t.Run(tt.name, func(t *testing.T) { 398 _, err := NewRecordingRule(tt.rule) 399 if tt.wantErr != "" { 400 require.Error(t, err) 401 require.EqualError(t, err, tt.wantErr) 402 } else { 403 require.NoError(t, err) 404 } 405 }) 406 } 407 }