Skip to content

Commit 3b010f7

Browse files
parse variables in expressions
1 parent 6ea382c commit 3b010f7

File tree

4 files changed

+86
-13
lines changed

4 files changed

+86
-13
lines changed

lexer.go

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,12 @@ again:
122122
}
123123
goto tokenFoundLabel
124124
}
125-
if strings.HasPrefix(s, "$__interval") {
126-
lex.sTail = s[len("$__interval"):]
127-
return "$__interval", nil
128-
}
129-
if strings.HasPrefix(s, "$__rate_interval") {
130-
lex.sTail = s[len("$__rate_interval"):]
131-
return "$__interval", nil
125+
if isVariable(s) {
126+
token, err = scanVariable(s)
127+
if err != nil {
128+
return "", err
129+
}
130+
goto tokenFoundLabel
132131
}
133132
return "", fmt.Errorf("cannot recognize %q", s)
134133

@@ -301,6 +300,43 @@ func scanPositiveNumber(s string) (string, error) {
301300
return s[:j], nil
302301
}
303302

303+
func isVariable(s string) bool {
304+
return len(s) > 1 && s[0] == '$'
305+
}
306+
307+
func isVariableChar(c byte) bool {
308+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'
309+
}
310+
311+
func scanVariable(s string) (string, error) {
312+
if len(s) < 2 {
313+
return "", fmt.Errorf("too small string for a variable %q", s)
314+
}
315+
i := 1
316+
hasBraces := s[i] == '{'
317+
if hasBraces {
318+
i++
319+
}
320+
for i < len(s) {
321+
if hasBraces {
322+
if s[i] == '}' {
323+
i++
324+
break
325+
}
326+
if !isVariableChar(s[i]) {
327+
return "", fmt.Errorf("not allowed symbol in variable %q", s)
328+
}
329+
} else if !isVariableChar(s[i]) {
330+
break
331+
}
332+
i++
333+
}
334+
if hasBraces && i <= 3 || i <= 2 {
335+
return "", fmt.Errorf("impossible variable name %q", s)
336+
}
337+
return s[:i], nil
338+
}
339+
304340
func scanNumMultiplier(s string) int {
305341
if len(s) > 3 {
306342
s = s[:3]
@@ -534,7 +570,7 @@ func scanSpecialIntegerPrefix(s string) (skipChars int, isHex bool) {
534570
}
535571

536572
func isPositiveDuration(s string) bool {
537-
if s == "$__interval" {
573+
if s == "$__interval" || s == "$__rate_interval" {
538574
return true
539575
}
540576
n := scanDuration(s)
@@ -610,7 +646,7 @@ func DurationValue(s string, step int64) (int64, error) {
610646
}
611647

612648
func parseSingleDuration(s string, step int64) (float64, error) {
613-
if s == "$__interval" {
649+
if s == "$__interval" || s == "$__rate_interval" {
614650
return float64(step), nil
615651
}
616652

@@ -676,8 +712,8 @@ func scanSingleDuration(s string, canBeNegative bool) int {
676712
if s[0] == '-' && canBeNegative {
677713
i++
678714
}
679-
if s[i:] == "$__interval" {
680-
return i + len("$__interval")
715+
if s[i:] == "$__interval" || s[i:] == "$__rate_interval" {
716+
return i + len(s[i:])
681717
}
682718
for i < len(s) && isDecimalChar(s[i]) {
683719
i++

lexer_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,33 @@ func TestScanPositiveNumberFailure(t *testing.T) {
9898
f("12.34e-")
9999
}
100100

101+
func TestScanVariableSuccess(t *testing.T) {
102+
f := func(s, vsExpected string) {
103+
t.Helper()
104+
vs, err := scanVariable(s)
105+
if err != nil {
106+
t.Fatalf("unexpected error in scanVariable(%q): %s", s, err)
107+
}
108+
if vs != vsExpected {
109+
t.Fatalf("unexpected variable scanned from %q; got %q; want %q", s, vs, vsExpected)
110+
}
111+
}
112+
f("$__rate_interval", "$__rate_interval")
113+
f("${foobar},test", "${foobar}")
114+
}
115+
116+
func TestScanVariableFailure(t *testing.T) {
117+
f := func(s string) {
118+
t.Helper()
119+
vs, err := scanVariable(s)
120+
if err == nil {
121+
t.Fatalf("expecting non-nil error in scanVariable(%q); got result %q", s, vs)
122+
}
123+
}
124+
f("")
125+
f("${foobar,")
126+
}
127+
101128
func TestParsePositiveNumberSuccess(t *testing.T) {
102129
f := func(s string, vExpected float64) {
103130
t.Helper()

parser.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ func (p *parser) parseSingleExprWithoutRollupSuffix() (Expr, error) {
473473
if isIdentPrefix(p.lex.Token) {
474474
return p.parseIdentExpr()
475475
}
476+
if isVariable(p.lex.Token) {
477+
e := &NumberExpr{
478+
s: p.lex.Token,
479+
}
480+
if err := p.lex.Next(); err != nil {
481+
return nil, err
482+
}
483+
return e, nil
484+
}
476485
switch p.lex.Token {
477486
case "(":
478487
return p.parseParensExpr()
@@ -1537,7 +1546,7 @@ func (p *parser) parseWindowAndStep() (*DurationExpr, *DurationExpr, bool, error
15371546
}
15381547
var window *DurationExpr
15391548
if !strings.HasPrefix(p.lex.Token, ":") {
1540-
if p.lex.Token == "$__interval" {
1549+
if p.lex.Token == "$__interval" || p.lex.Token == "$__rate_interval" {
15411550
// Skip $__interval, since it must be treated as missing lookbehind window,
15421551
// e.g. rate(m[$__interval]) must be equivalent to rate(m).
15431552
// In this case VictoriaMetrics automatically adjusts the lookbehind window
@@ -1656,7 +1665,7 @@ func (p *parser) parsePositiveDuration() (*DurationExpr, error) {
16561665
}
16571666
}
16581667
// Verify duration value.
1659-
if s == "$__interval" {
1668+
if s == "$__interval" || s == "$__rate_interval" {
16601669
s = "1i"
16611670
}
16621671
return newDurationExpr(s)

parser_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func TestParseSuccess(t *testing.T) {
2323
}
2424

2525
// metricExpr
26+
same(`topk($topk, max(sum(vmalert_recording_rules_last_evaluation_samples{job=~"$job",instance=~"$instance",group=~"$group",file=~"$file"}) by(job,instance,group,file,recording) > 0) by(job,group,file,recording))`)
2627
same(`{}`)
2728
same(`{}[5m]`)
2829
same(`{}[5m:]`)

0 commit comments

Comments
 (0)