github.com/thanos-io/thanos@v0.32.5/pkg/querysharding/analyzer.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  // Copyright 2013 The Prometheus Authors
     5  // Licensed under the Apache License, Version 2.0 (the "License");
     6  // you may not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  // http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing, software
    12  // distributed under the License is distributed on an "AS IS" BASIS,
    13  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  // See the License for the specific language governing permissions and
    15  // limitations under the License.
    16  
    17  package querysharding
    18  
    19  import (
    20  	"fmt"
    21  
    22  	lru "github.com/hashicorp/golang-lru"
    23  	"github.com/prometheus/common/model"
    24  	"github.com/prometheus/prometheus/promql/parser"
    25  )
    26  
    27  var (
    28  	notShardableErr = fmt.Errorf("expressions are not shardable")
    29  )
    30  
    31  type Analyzer interface {
    32  	Analyze(string) (QueryAnalysis, error)
    33  }
    34  
    35  // QueryAnalyzer is an analyzer which determines
    36  // whether a PromQL Query is shardable and using which labels.
    37  type QueryAnalyzer struct{}
    38  
    39  type CachedQueryAnalyzer struct {
    40  	analyzer *QueryAnalyzer
    41  	cache    *lru.Cache
    42  }
    43  
    44  // NewQueryAnalyzer creates a new QueryAnalyzer.
    45  func NewQueryAnalyzer() *CachedQueryAnalyzer {
    46  	// Ignore the error check since it throws error
    47  	// only if size is <= 0.
    48  	cache, _ := lru.New(256)
    49  	return &CachedQueryAnalyzer{
    50  		analyzer: &QueryAnalyzer{},
    51  		cache:    cache,
    52  	}
    53  }
    54  
    55  type cachedValue struct {
    56  	QueryAnalysis QueryAnalysis
    57  	err           error
    58  }
    59  
    60  func (a *CachedQueryAnalyzer) Analyze(query string) (QueryAnalysis, error) {
    61  	if a.cache.Contains(query) {
    62  		value, ok := a.cache.Get(query)
    63  		if ok {
    64  			return value.(cachedValue).QueryAnalysis, value.(cachedValue).err
    65  		}
    66  	}
    67  
    68  	// Analyze if needed.
    69  	analysis, err := a.analyzer.Analyze(query)
    70  
    71  	// Adding to cache.
    72  	_ = a.cache.Add(query, cachedValue{QueryAnalysis: analysis, err: err})
    73  
    74  	return analysis, err
    75  }
    76  
    77  // Analyze analyzes a query and returns a QueryAnalysis.
    78  
    79  // Analyze uses the following algorithm:
    80  //   - if a query has functions which cannot be sharded such as
    81  //     absent or absent_over_time, then treat the query as non shardable.
    82  //   - if a query has functions `label_join` or `label_replace`,
    83  //     calculate the shard labels based on grouping labels.
    84  //   - Walk the query and find the least common labelset
    85  //     used in grouping expressions. If non-empty, treat the query
    86  //     as shardable by those labels.
    87  //   - otherwise, treat the query as non-shardable.
    88  //
    89  // The le label is excluded from sharding.
    90  func (a *QueryAnalyzer) Analyze(query string) (QueryAnalysis, error) {
    91  	expr, err := parser.ParseExpr(query)
    92  	if err != nil {
    93  		return nonShardableQuery(), err
    94  	}
    95  
    96  	var (
    97  		analysis      QueryAnalysis
    98  		dynamicLabels []string
    99  	)
   100  	isShardable := true
   101  	parser.Inspect(expr, func(node parser.Node, nodes []parser.Node) error {
   102  		switch n := node.(type) {
   103  		case *parser.Call:
   104  			if n.Func != nil {
   105  				if n.Func.Name == "label_join" || n.Func.Name == "label_replace" {
   106  					dstLabel := stringFromArg(n.Args[1])
   107  					dynamicLabels = append(dynamicLabels, dstLabel)
   108  				} else if n.Func.Name == "absent_over_time" || n.Func.Name == "absent" || n.Func.Name == "scalar" {
   109  					isShardable = false
   110  					return notShardableErr
   111  				}
   112  			}
   113  		case *parser.BinaryExpr:
   114  			if n.VectorMatching != nil {
   115  				shardingLabels := without(n.VectorMatching.MatchingLabels, []string{"le"})
   116  				if !n.VectorMatching.On && len(shardingLabels) > 0 {
   117  					shardingLabels = append(shardingLabels, model.MetricNameLabel)
   118  				}
   119  				analysis = analysis.scopeToLabels(shardingLabels, n.VectorMatching.On)
   120  			}
   121  		case *parser.AggregateExpr:
   122  			shardingLabels := make([]string, 0)
   123  			if len(n.Grouping) > 0 {
   124  				shardingLabels = without(n.Grouping, []string{"le"})
   125  			}
   126  			analysis = analysis.scopeToLabels(shardingLabels, !n.Without)
   127  		}
   128  
   129  		return nil
   130  	})
   131  
   132  	if !isShardable {
   133  		return nonShardableQuery(), nil
   134  	}
   135  
   136  	// If currently it is shard by, it is still shardable if there is
   137  	// any label left after removing the dynamic labels.
   138  	// If currently it is shard without, it is still shardable if we
   139  	// shard without the union of the labels.
   140  	// TODO(yeya24): we can still make dynamic labels shardable if we push
   141  	// down the label_replace and label_join computation to the store level.
   142  	if len(dynamicLabels) > 0 {
   143  		analysis = analysis.scopeToLabels(dynamicLabels, false)
   144  	}
   145  
   146  	return analysis, nil
   147  }
   148  
   149  // Copied from https://github.com/prometheus/prometheus/blob/v2.40.1/promql/functions.go#L1416.
   150  func stringFromArg(e parser.Expr) string {
   151  	tmp := unwrapStepInvariantExpr(e) // Unwrap StepInvariant
   152  	unwrapParenExpr(&tmp)             // Optionally unwrap ParenExpr
   153  	return tmp.(*parser.StringLiteral).Val
   154  }
   155  
   156  // Copied from https://github.com/prometheus/prometheus/blob/v2.40.1/promql/engine.go#L2642.
   157  // unwrapParenExpr does the AST equivalent of removing parentheses around a expression.
   158  func unwrapParenExpr(e *parser.Expr) {
   159  	for {
   160  		if p, ok := (*e).(*parser.ParenExpr); ok {
   161  			*e = p.Expr
   162  		} else {
   163  			break
   164  		}
   165  	}
   166  }
   167  
   168  // Copied from https://github.com/prometheus/prometheus/blob/v2.40.1/promql/engine.go#L2652.
   169  func unwrapStepInvariantExpr(e parser.Expr) parser.Expr {
   170  	if p, ok := e.(*parser.StepInvariantExpr); ok {
   171  		return p.Expr
   172  	}
   173  	return e
   174  }