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 }