thanos-io/thanos · error

parser ParseMetricSelector

Error message

parser ParseMetricSelector

What it means

After proxying, the handler parses every MatcherString in the request as a PromQL metric selector via extpromql.ParseMetricSelector; a parse failure is wrapped as 'parser ParseMetricSelector'. It means one of the requested matcher strings is not a valid metric selector like {job="foo",env=~"prod.*"}.

Solutions

  1. Validate each matcher string with promql.ParseMetricSelector client-side before calling the API
  2. Fix malformed selectors (balanced braces, quoted values, valid matchers =,!=,=~,!~)
  3. Use the SDK/helper to construct matcher sets programmatically instead of string concatenation

Example fix

// before
req.MatcherString = []string{"{job=prod}"} // invalid: unquoted value
// after
req.MatcherString = []string{"{job=\"prod\"}"}
Defensive patterns

Strategy: validation

Validate before calling

for _, s := range matchers {
    if _, err := promql.ParseMetricSelector(s); err != nil {
        return fmt.Errorf("invalid matcher %q: %w", s, err)
    }
}

Type guard

func isValidMetricSelector(s string) bool {
    _, err := extpromql.ParseMetricSelector(s)
    return err == nil
}

Prevention

When it happens

Trigger: Calling the Rules API with req.MatcherString entries that are empty strings or not parseable PromQL selectors (unbalanced braces, invalid label matchers, bad regex).

Common situations: Hand-built matcher strings from user input; shell quoting stripping braces or quotes; passing label names or LogQL instead of PromQL selectors; upgrading promql parser versions that reject previously tolerated syntax.

Understand the failure class

Background: "Invalid query parameter" / "Failed to parse value of ...": fixing bad query string parameters across APIs — this error's family across 36 libraries.

Related errors


AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07). Data as JSON: /api/errors/1b5c068e6146ae21. Report an issue: GitHub.

Appendix: source

Thrown at pkg/rules/rules.go:69

	return c
}

func (rr *GRPCClient) Rules(ctx context.Context, req *rulespb.RulesRequest) (*rulespb.RuleGroups, annotations.Annotations, error) {
	span, ctx := tracing.StartSpan(ctx, "rules_request")
	defer span.Finish()

	resp := &rulesServer{ctx: ctx}

	if err := rr.proxy.Rules(req, resp); err != nil {
		return nil, nil, errors.Wrap(err, "proxy Rules")
	}

	var err error
	matcherSets := make([][]*labels.Matcher, len(req.MatcherString))
	for i, s := range req.MatcherString {
		matcherSets[i], err = extpromql.ParseMetricSelector(s)
		if err != nil {
			return nil, nil, errors.Wrap(err, "parser ParseMetricSelector")
		}
	}

	resp.groups = filterRulesByMatchers(resp.groups, matcherSets)
	resp.groups = filterRulesByNamesAndFile(resp.groups, req.RuleName, req.RuleGroup, req.File)

	// TODO(bwplotka): Move to SortInterface with equal method and heap.
	resp.groups = dedupGroups(resp.groups)
	for _, g := range resp.groups {
		g.Rules = dedupRules(g.Rules, rr.replicaLabels)
	}

	return &rulespb.RuleGroups{Groups: resp.groups}, resp.warnings, nil
}

// filters rules by group name, rule name or file.
func filterRulesByNamesAndFile(ruleGroups []*rulespb.RuleGroup, ruleName []string, ruleGroup []string, file []string) []*rulespb.RuleGroup {
	if len(ruleName) == 0 && len(ruleGroup) == 0 && len(file) == 0 {

View on GitHub (pinned to 35b8b99117)