diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index ef9135d14..e2dd8015c 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -612,11 +612,10 @@ func parseExprWithoutPipe(e string) (Expr, string, error) { return nil, "", ErrMissingExpr } - if '0' <= e[0] && e[0] <= '9' || e[0] == '-' || e[0] == '+' { - val, valStr, e, err := parseConst(e) - r, _ := utf8.DecodeRuneInString(e) - if !unicode.IsLetter(r) { - return &expr{val: val, etype: EtConst, valStr: valStr}, e, err + if IsDigit(e[0]) || e[0] == '-' || e[0] == '+' { + exp, rest, ok := parseConstExpr(e) + if ok { + return exp, rest, nil } } @@ -655,6 +654,25 @@ func parseExprWithoutPipe(e string) (Expr, string, error) { return &expr{target: name}, e, nil } +func parseConstExpr(e string) (*expr, string, bool) { + val, valStr, rest, err := parseConst(e) + if err != nil { + // A failure means the greedily-slurped run of [0-9.+-eE] characters is + // not a valid float, e.g. a root metric name like "587-AL". Let the + // caller parse the original token as a metric name instead. + return nil, e, false + } + + if rest != "" { + r, size := utf8.DecodeRuneInString(rest) + if size > 0 && unicode.IsLetter(r) { + return nil, e, false + } + } + + return &expr{val: val, etype: EtConst, valStr: valStr}, rest, true +} + func parseExprInner(e string) (Expr, string, error) { exp, e, err := parseExprWithoutPipe(e) if err != nil { diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 1966b31bf..d8f2b0b57 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -165,6 +165,58 @@ func TestParseExpr(t *testing.T) { }, }, + // Issue #524: a root metric name that starts with one or more digits + // followed by a dash (e.g. "587-AL") must parse as a metric name, the + // same as graphite-web, instead of failing ParseFloat on "587-". + { + "587-AL.random.a", + &expr{target: "587-AL.random.a"}, + }, + { + "587-AL", + &expr{target: "587-AL"}, + }, + // Guard against over-correction: these already parsed as names and + // must keep doing so. + { + "AAA-587", + &expr{target: "AAA-587"}, + }, + { + "587AAA", + &expr{target: "587AAA"}, + }, + { + "AAA587", + &expr{target: "AAA587"}, + }, + // Genuine numeric constants must still parse as EtConst. + { + "-1", + &expr{val: -1, etype: EtConst, valStr: "-1"}, + }, + { + "+2.5", + &expr{val: 2.5, etype: EtConst, valStr: "+2.5"}, + }, + { + "1e3", + &expr{val: 1000, etype: EtConst, valStr: "1e3"}, + }, + // A function call with a digit-dash series arg parses without error and + // preserves the inner metric name. + { + "sumSeries(587-AL.random.*)", + &expr{ + target: "sumSeries", + etype: EtFunc, + args: []*expr{ + {target: "587-AL.random.*"}, + }, + argString: "587-AL.random.*", + }, + }, + { "func1(metric1, -3 , 'foo' )", &expr{