github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/logql/syntax/ast_test.go (about) 1 package syntax 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 "github.com/prometheus/prometheus/model/labels" 9 "github.com/prometheus/prometheus/promql" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/grafana/loki/pkg/logql/log" 14 ) 15 16 var labelBar, _ = ParseLabels("{app=\"bar\"}") 17 18 func Test_logSelectorExpr_String(t *testing.T) { 19 t.Parallel() 20 tests := []struct { 21 selector string 22 expectFilter bool 23 }{ 24 {`{foo="bar"}`, false}, 25 {`{foo="bar", bar!="baz"}`, false}, 26 {`{foo="bar", bar!="baz"} != "bip" !~ ".+bop"`, true}, 27 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap"`, true}, 28 {`{foo="bar", bar!="baz"} |= ""`, false}, 29 {`{foo="bar", bar!="baz"} |= "" |= ip("::1")`, true}, 30 {`{foo="bar", bar!="baz"} |= "" != ip("127.0.0.1")`, true}, 31 {`{foo="bar", bar!="baz"} |~ ""`, false}, 32 {`{foo="bar", bar!="baz"} |~ ".*"`, false}, 33 {`{foo="bar", bar!="baz"} |= "" |= ""`, false}, 34 {`{foo="bar", bar!="baz"} |~ "" |= "" |~ ".*"`, false}, 35 {`{foo="bar", bar!="baz"} != "bip" !~ ".+bop" | json`, true}, 36 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | logfmt`, true}, 37 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | unpack | foo>5`, true}, 38 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | pattern "<foo> bar <buzz>" | foo>5`, true}, 39 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | logfmt | b>=10GB`, true}, 40 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | logfmt | b=ip("127.0.0.1")`, true}, 41 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | logfmt | b=ip("127.0.0.1") | level="error"`, true}, 42 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | logfmt | b=ip("127.0.0.1") | level="error" | c=ip("::1")`, true}, // chain inside label filters. 43 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | regexp "(?P<foo>foo|bar)"`, true}, 44 {`{foo="bar"} |= "baz" |~ "blip" != "flip" !~ "flap" | regexp "(?P<foo>foo|bar)" | ( ( foo<5.01 , bar>20ms ) or foo="bar" ) | line_format "blip{{.boop}}bap" | label_format foo=bar,bar="blip{{.blop}}"`, true}, 45 } 46 47 for _, tt := range tests { 48 tt := tt 49 t.Run(tt.selector, func(t *testing.T) { 50 t.Parallel() 51 expr, err := ParseLogSelector(tt.selector, true) 52 if err != nil { 53 t.Fatalf("failed to parse log selector: %s", err) 54 } 55 p, err := expr.Pipeline() 56 if err != nil { 57 t.Fatalf("failed to get filter: %s", err) 58 } 59 if !tt.expectFilter { 60 require.Equal(t, log.NewNoopPipeline(), p) 61 } 62 if expr.String() != tt.selector { 63 t.Fatalf("error expected: %s got: %s", tt.selector, expr.String()) 64 } 65 }) 66 } 67 } 68 69 func Test_SampleExpr_String(t *testing.T) { 70 t.Parallel() 71 for _, tc := range []string{ 72 `rate( ( {job="mysql"} |="error" !="timeout" ) [10s] )`, 73 `absent_over_time( ( {job="mysql"} |="error" !="timeout" ) [10s] )`, 74 `absent_over_time( ( {job="mysql"} |="error" !="timeout" ) [10s] offset 10d )`, 75 `sum without(a) ( rate ( ( {job="mysql"} |="error" !="timeout" ) [10s] ) )`, 76 `sum by(a) (rate( ( {job="mysql"} |="error" !="timeout" ) [10s] ) )`, 77 `sum(count_over_time({job="mysql"}[5m]))`, 78 `sum(count_over_time({job="mysql"}[5m] offset 10m))`, 79 `sum(count_over_time({job="mysql"} | json [5m]))`, 80 `sum(count_over_time({job="mysql"} | json [5m] offset 10m))`, 81 `sum(count_over_time({job="mysql"} | logfmt [5m]))`, 82 `sum(count_over_time({job="mysql"} | logfmt [5m] offset 10m))`, 83 `sum(count_over_time({job="mysql"} | pattern "<foo> bar <buzz>" | json [5m]))`, 84 `sum(count_over_time({job="mysql"} | unpack | json [5m]))`, 85 `sum(count_over_time({job="mysql"} | regexp "(?P<foo>foo|bar)" [5m]))`, 86 `sum(count_over_time({job="mysql"} | regexp "(?P<foo>foo|bar)" [5m] offset 10y))`, 87 `topk(10,sum(rate({region="us-east1"}[5m])) by (name))`, 88 `topk by (name)(10,sum(rate({region="us-east1"}[5m])))`, 89 `avg( rate( ( {job="nginx"} |= "GET" ) [10s] ) ) by (region)`, 90 `avg(min_over_time({job="nginx"} |= "GET" | unwrap foo[10s])) by (region)`, 91 `avg(min_over_time({job="nginx"} |= "GET" | unwrap foo[10s] offset 10m)) by (region)`, 92 `sum by (cluster) (count_over_time({job="mysql"}[5m]))`, 93 `sum by (cluster) (count_over_time({job="mysql"}[5m] offset 10m))`, 94 `sum by (cluster) (count_over_time({job="mysql"}[5m])) / sum by (cluster) (count_over_time({job="postgres"}[5m])) `, 95 `sum by (cluster) (count_over_time({job="mysql"}[5m] offset 10m)) / sum by (cluster) (count_over_time({job="postgres"}[5m] offset 10m)) `, 96 ` 97 sum by (cluster) (count_over_time({job="postgres"}[5m])) / 98 sum by (cluster) (count_over_time({job="postgres"}[5m])) / 99 sum by (cluster) (count_over_time({job="postgres"}[5m])) 100 `, 101 `sum by (cluster) (count_over_time({job="mysql"}[5m])) / min(count_over_time({job="mysql"}[5m])) `, 102 `sum by (job) ( 103 count_over_time({namespace="tns"} |= "level=error"[5m]) 104 / 105 count_over_time({namespace="tns"}[5m]) 106 )`, 107 `stdvar_over_time({app="foo"} |= "bar" | json | latency >= 250ms or ( status_code < 500 and status_code > 200) 108 | line_format "blip{{ .foo }}blop {{.status_code}}" | label_format foo=bar,status_code="buzz{{.bar}}" | unwrap foo [5m])`, 109 `stdvar_over_time({app="foo"} |= "bar" | json | latency >= 250ms or ( status_code < 500 and status_code > 200) 110 | line_format "blip{{ .foo }}blop {{.status_code}}" | label_format foo=bar,status_code="buzz{{.bar}}" | unwrap foo [5m] offset 10m)`, 111 `sum_over_time({namespace="tns"} |= "level=error" | json |foo>=5,bar<25ms|unwrap latency [5m])`, 112 `sum by (job) ( 113 sum_over_time({namespace="tns"} |= "level=error" | json | foo=5 and bar<25ms | unwrap latency[5m]) 114 / 115 count_over_time({namespace="tns"} | logfmt | label_format foo=bar[5m]) 116 )`, 117 `sum by (job) ( 118 sum_over_time({namespace="tns"} |= "level=error" | json | foo=5 and bar<25ms | unwrap bytes(latency)[5m]) 119 / 120 count_over_time({namespace="tns"} | logfmt | label_format foo=bar[5m]) 121 )`, 122 `sum by (job) ( 123 sum_over_time( 124 {namespace="tns"} |= "level=error" | json | avg=5 and bar<25ms | unwrap duration(latency) [5m] 125 ) 126 / 127 count_over_time({namespace="tns"} | logfmt | label_format foo=bar[5m]) 128 )`, 129 `sum_over_time({namespace="tns"} |= "level=error" | json |foo>=5,bar<25ms | unwrap latency | __error__!~".*" | foo >5[5m])`, 130 `last_over_time({namespace="tns"} |= "level=error" | json |foo>=5,bar<25ms | unwrap latency | __error__!~".*" | foo >5[5m])`, 131 `first_over_time({namespace="tns"} |= "level=error" | json |foo>=5,bar<25ms | unwrap latency | __error__!~".*" | foo >5[5m])`, 132 `absent_over_time({namespace="tns"} |= "level=error" | json |foo>=5,bar<25ms | unwrap latency | __error__!~".*" | foo >5[5m])`, 133 `sum by (job) ( 134 sum_over_time( 135 {namespace="tns"} |= "level=error" | json | avg=5 and bar<25ms | unwrap duration(latency) | __error__!~".*" [5m] 136 ) 137 / 138 count_over_time({namespace="tns"} | logfmt | label_format foo=bar[5m]) 139 )`, 140 `label_replace( 141 sum by (job) ( 142 sum_over_time( 143 {namespace="tns"} |= "level=error" | json | avg=5 and bar<25ms | unwrap duration(latency) | __error__!~".*" [5m] 144 ) 145 / 146 count_over_time({namespace="tns"} | logfmt | label_format foo=bar[5m]) 147 ), 148 "foo", 149 "$1", 150 "service", 151 "(.*):.*" 152 ) 153 `, 154 `10 / (5/2)`, 155 `10 / (count_over_time({job="postgres"}[5m])/2)`, 156 `{app="foo"} | json response_status="response.status.code", first_param="request.params[0]"`, 157 `label_replace( 158 sum by (job) ( 159 sum_over_time( 160 {namespace="tns"} |= "level=error" | json | avg=5 and bar<25ms | unwrap duration(latency) | __error__!~".*" [5m] offset 1h 161 ) 162 / 163 count_over_time({namespace="tns"} | logfmt | label_format foo=bar[5m] offset 1h) 164 ), 165 "foo", 166 "$1", 167 "service", 168 "(.*):.*" 169 ) 170 `, 171 } { 172 t.Run(tc, func(t *testing.T) { 173 expr, err := ParseExpr(tc) 174 require.Nil(t, err) 175 176 expr2, err := ParseExpr(expr.String()) 177 require.Nil(t, err) 178 require.Equal(t, expr, expr2) 179 }) 180 } 181 } 182 183 func TestMatcherGroups(t *testing.T) { 184 for i, tc := range []struct { 185 query string 186 exp []MatcherRange 187 }{ 188 { 189 query: `{job="foo"}`, 190 exp: []MatcherRange{ 191 { 192 Matchers: []*labels.Matcher{ 193 labels.MustNewMatcher(labels.MatchEqual, "job", "foo"), 194 }, 195 }, 196 }, 197 }, 198 { 199 query: `count_over_time({job="foo"}[5m]) / count_over_time({job="bar"}[5m] offset 10m)`, 200 exp: []MatcherRange{ 201 { 202 Interval: 5 * time.Minute, 203 Matchers: []*labels.Matcher{ 204 labels.MustNewMatcher(labels.MatchEqual, "job", "foo"), 205 }, 206 }, 207 { 208 Interval: 5 * time.Minute, 209 Offset: 10 * time.Minute, 210 Matchers: []*labels.Matcher{ 211 labels.MustNewMatcher(labels.MatchEqual, "job", "bar"), 212 }, 213 }, 214 }, 215 }, 216 } { 217 t.Run(fmt.Sprint(i), func(t *testing.T) { 218 expr, err := ParseExpr(tc.query) 219 require.Nil(t, err) 220 out := MatcherGroups(expr) 221 require.Equal(t, tc.exp, out) 222 }) 223 } 224 } 225 226 func Test_NilFilterDoesntPanic(t *testing.T) { 227 t.Parallel() 228 for _, tc := range []string{ 229 `{namespace="dev", container_name="cart"} |= "" |= "bloop"`, 230 `{namespace="dev", container_name="cart"} |= "bleep" |= ""`, 231 `{namespace="dev", container_name="cart"} |= "bleep" |= "" |= "bloop"`, 232 `{namespace="dev", container_name="cart"} |= "bleep" |= "" |= "bloop"`, 233 `{namespace="dev", container_name="cart"} |= "bleep" |= "bloop" |= ""`, 234 } { 235 t.Run(tc, func(t *testing.T) { 236 expr, err := ParseLogSelector(tc, true) 237 require.Nil(t, err) 238 239 p, err := expr.Pipeline() 240 require.Nil(t, err) 241 _, _, matches := p.ForStream(labelBar).Process(0, []byte("bleepbloop")) 242 243 require.True(t, matches) 244 }) 245 } 246 } 247 248 type linecheck struct { 249 l string 250 e bool 251 } 252 253 func Test_FilterMatcher(t *testing.T) { 254 t.Parallel() 255 for _, tt := range []struct { 256 q string 257 258 expectedMatchers []*labels.Matcher 259 // test line against the resulting filter, if empty filter should also be nil 260 lines []linecheck 261 }{ 262 { 263 `{app="foo",cluster=~".+bar"}`, 264 []*labels.Matcher{ 265 mustNewMatcher(labels.MatchEqual, "app", "foo"), 266 mustNewMatcher(labels.MatchRegexp, "cluster", ".+bar"), 267 }, 268 nil, 269 }, 270 { 271 `{app!="foo",cluster=~".+bar",bar!~".?boo"}`, 272 []*labels.Matcher{ 273 mustNewMatcher(labels.MatchNotEqual, "app", "foo"), 274 mustNewMatcher(labels.MatchRegexp, "cluster", ".+bar"), 275 mustNewMatcher(labels.MatchNotRegexp, "bar", ".?boo"), 276 }, 277 nil, 278 }, 279 { 280 `{app="foo"} |= "foo"`, 281 []*labels.Matcher{ 282 mustNewMatcher(labels.MatchEqual, "app", "foo"), 283 }, 284 []linecheck{{"foobar", true}, {"bar", false}}, 285 }, 286 { 287 `{app="foo"} |= "foo" != "bar"`, 288 []*labels.Matcher{ 289 mustNewMatcher(labels.MatchEqual, "app", "foo"), 290 }, 291 []linecheck{{"foobuzz", true}, {"bar", false}}, 292 }, 293 { 294 `{app="foo"} |= "foo" !~ "f.*b"`, 295 []*labels.Matcher{ 296 mustNewMatcher(labels.MatchEqual, "app", "foo"), 297 }, 298 []linecheck{{"foo", true}, {"bar", false}, {"foobar", false}}, 299 }, 300 { 301 `{app="foo"} |= "foo" |~ "f.*b"`, 302 []*labels.Matcher{ 303 mustNewMatcher(labels.MatchEqual, "app", "foo"), 304 }, 305 []linecheck{{"foo", false}, {"bar", false}, {"foobar", true}}, 306 }, 307 { 308 `{app="foo"} |~ "foo"`, 309 []*labels.Matcher{ 310 mustNewMatcher(labels.MatchEqual, "app", "foo"), 311 }, 312 []linecheck{{"foo", true}, {"bar", false}, {"foobar", true}}, 313 }, 314 { 315 `{app="foo"} | logfmt | duration > 1s and total_bytes < 1GB`, 316 []*labels.Matcher{ 317 mustNewMatcher(labels.MatchEqual, "app", "foo"), 318 }, 319 []linecheck{{"duration=5m total_bytes=5kB", true}, {"duration=1s total_bytes=256B", false}, {"duration=0s", false}}, 320 }, 321 } { 322 tt := tt 323 t.Run(tt.q, func(t *testing.T) { 324 t.Parallel() 325 expr, err := ParseLogSelector(tt.q, true) 326 assert.Nil(t, err) 327 assert.Equal(t, tt.expectedMatchers, expr.Matchers()) 328 p, err := expr.Pipeline() 329 assert.Nil(t, err) 330 if tt.lines == nil { 331 assert.Equal(t, p, log.NewNoopPipeline()) 332 } else { 333 sp := p.ForStream(labelBar) 334 for _, lc := range tt.lines { 335 _, _, matches := sp.Process(0, []byte(lc.l)) 336 assert.Equalf(t, lc.e, matches, "query for line '%s' was %v and not %v", lc.l, matches, lc.e) 337 } 338 } 339 }) 340 } 341 } 342 343 func TestStringer(t *testing.T) { 344 for _, tc := range []struct { 345 in string 346 out string 347 }{ 348 { 349 in: `1 > 1 > 1`, 350 out: `0`, 351 }, 352 { 353 in: `1.6`, 354 out: `1.6`, 355 }, 356 { 357 in: `1 > 1 > bool 1`, 358 out: `0`, 359 }, 360 { 361 in: `1 > bool 1 > count_over_time({foo="bar"}[1m])`, 362 out: `(0 > count_over_time({foo="bar"}[1m]))`, 363 }, 364 { 365 in: `1 > bool 1 > bool count_over_time({foo="bar"}[1m])`, 366 out: `(0 > bool count_over_time({foo="bar"}[1m]))`, 367 }, 368 { 369 in: `0 > count_over_time({foo="bar"}[1m])`, 370 out: `(0 > count_over_time({foo="bar"}[1m]))`, 371 }, 372 } { 373 t.Run(tc.in, func(t *testing.T) { 374 expr, err := ParseExpr(tc.in) 375 require.Nil(t, err) 376 require.Equal(t, tc.out, expr.String()) 377 }) 378 } 379 } 380 381 func BenchmarkContainsFilter(b *testing.B) { 382 lines := [][]byte{ 383 []byte("hello world foo bar"), 384 []byte("bar hello world for"), 385 []byte("hello world foobar and the bar and more bar until the end"), 386 []byte("hello world foobar and the bar and more bar and more than one hundred characters for sure until the end"), 387 []byte("hello world foobar and the bar and more bar and more than one hundred characters for sure until the end and yes bar"), 388 } 389 390 benchmarks := []struct { 391 name string 392 expr string 393 }{ 394 { 395 "AllMatches", 396 `{app="foo"} |= "foo" |= "hello" |= "world" |= "bar"`, 397 }, 398 { 399 "OneMatches", 400 `{app="foo"} |= "foo" |= "not" |= "in" |= "there"`, 401 }, 402 { 403 "MixedFiltersTrue", 404 `{app="foo"} |= "foo" != "not" |~ "hello.*bar" != "there" |= "world"`, 405 }, 406 { 407 "MixedFiltersFalse", 408 `{app="foo"} |= "baz" != "not" |~ "hello.*bar" != "there" |= "world"`, 409 }, 410 { 411 "GreedyRegex", 412 `{app="foo"} |~ "hello.*bar.*"`, 413 }, 414 { 415 "NonGreedyRegex", 416 `{app="foo"} |~ "hello.*?bar.*?"`, 417 }, 418 { 419 "ReorderedRegex", 420 `{app="foo"} |~ "hello.*?bar.*?" |= "not"`, 421 }, 422 } 423 424 for _, bm := range benchmarks { 425 b.Run(bm.name, func(b *testing.B) { 426 expr, err := ParseLogSelector(bm.expr, false) 427 if err != nil { 428 b.Fatal(err) 429 } 430 431 p, err := expr.Pipeline() 432 if err != nil { 433 b.Fatal(err) 434 } 435 436 b.ResetTimer() 437 sp := p.ForStream(labelBar) 438 for i := 0; i < b.N; i++ { 439 for _, line := range lines { 440 sp.Process(0, line) 441 } 442 } 443 }) 444 } 445 } 446 447 func Test_parserExpr_Parser(t *testing.T) { 448 tests := []struct { 449 name string 450 op string 451 param string 452 want log.Stage 453 wantErr bool 454 }{ 455 {"json", OpParserTypeJSON, "", log.NewJSONParser(), false}, 456 {"unpack", OpParserTypeUnpack, "", log.NewUnpackParser(), false}, 457 {"logfmt", OpParserTypeLogfmt, "", log.NewLogfmtParser(), false}, 458 {"pattern", OpParserTypePattern, "<foo> bar <buzz>", mustNewPatternParser("<foo> bar <buzz>"), false}, 459 {"pattern err", OpParserTypePattern, "bar", nil, true}, 460 {"regexp", OpParserTypeRegexp, "(?P<foo>foo)", mustNewRegexParser("(?P<foo>foo)"), false}, 461 {"regexp err ", OpParserTypeRegexp, "foo", nil, true}, 462 } 463 for _, tt := range tests { 464 t.Run(tt.name, func(t *testing.T) { 465 e := &LabelParserExpr{ 466 Op: tt.op, 467 Param: tt.param, 468 } 469 got, err := e.Stage() 470 if (err != nil) != tt.wantErr { 471 t.Errorf("parserExpr.Parser() error = %v, wantErr %v", err, tt.wantErr) 472 return 473 } 474 if tt.wantErr { 475 require.Nil(t, got) 476 } else { 477 require.Equal(t, tt.want, got) 478 } 479 }) 480 } 481 } 482 483 func mustNewRegexParser(re string) log.Stage { 484 r, err := log.NewRegexpParser(re) 485 if err != nil { 486 panic(err) 487 } 488 return r 489 } 490 491 func mustNewPatternParser(p string) log.Stage { 492 r, err := log.NewPatternParser(p) 493 if err != nil { 494 panic(err) 495 } 496 return r 497 } 498 499 func Test_canInjectVectorGrouping(t *testing.T) { 500 tests := []struct { 501 vecOp string 502 rangeOp string 503 want bool 504 }{ 505 {OpTypeSum, OpRangeTypeBytes, true}, 506 {OpTypeSum, OpRangeTypeBytesRate, true}, 507 {OpTypeSum, OpRangeTypeSum, true}, 508 {OpTypeSum, OpRangeTypeRate, true}, 509 {OpTypeSum, OpRangeTypeCount, true}, 510 511 {OpTypeSum, OpRangeTypeAvg, false}, 512 {OpTypeSum, OpRangeTypeMax, false}, 513 {OpTypeSum, OpRangeTypeQuantile, false}, 514 {OpTypeSum, OpRangeTypeStddev, false}, 515 {OpTypeSum, OpRangeTypeStdvar, false}, 516 {OpTypeSum, OpRangeTypeMin, false}, 517 {OpTypeSum, OpRangeTypeMax, false}, 518 519 {OpTypeAvg, OpRangeTypeBytes, false}, 520 {OpTypeCount, OpRangeTypeBytesRate, false}, 521 {OpTypeBottomK, OpRangeTypeSum, false}, 522 {OpTypeMax, OpRangeTypeRate, false}, 523 {OpTypeMin, OpRangeTypeCount, false}, 524 {OpTypeTopK, OpRangeTypeCount, false}, 525 } 526 for _, tt := range tests { 527 t.Run(tt.vecOp+"_"+tt.rangeOp, func(t *testing.T) { 528 if got := canInjectVectorGrouping(tt.vecOp, tt.rangeOp); got != tt.want { 529 t.Errorf("canInjectVectorGrouping() = %v, want %v", got, tt.want) 530 } 531 }) 532 } 533 } 534 535 func Test_MergeBinOpVectors_Filter(t *testing.T) { 536 res := MergeBinOp( 537 OpTypeGT, 538 &promql.Sample{ 539 Point: promql.Point{V: 2}, 540 }, 541 &promql.Sample{ 542 Point: promql.Point{V: 0}, 543 }, 544 true, 545 true, 546 ) 547 548 // ensure we return the left hand side's value (2) instead of the 549 // comparison operator's result (1: the truthy answer) 550 require.Equal(t, &promql.Sample{ 551 Point: promql.Point{V: 2}, 552 }, res) 553 }