bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/expr/elastic2.go (about) 1 package expr 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "time" 8 9 "bosun.org/opentsdb" 10 elastic "gopkg.in/olivere/elastic.v3" 11 ) 12 13 // InitClient sets up the elastic client. If the client has already been 14 // initialized it is a noop 15 func (e ElasticHosts) InitClient2(prefix string) error { 16 if _, ok := e.Hosts[prefix]; !ok { 17 prefixes := make([]string, len(e.Hosts)) 18 i := 0 19 for k := range e.Hosts { 20 prefixes[i] = k 21 i++ 22 } 23 return fmt.Errorf("prefix %v not defined, available prefixes are: %v", prefix, prefixes) 24 } 25 if c := esClients.m[prefix]; c != nil { 26 // client already initialized 27 return nil 28 } 29 // esClients.Lock() 30 var err error 31 if e.Hosts[prefix].SimpleClient { 32 // simple client enabled 33 esClients.m[prefix], err = elastic.NewSimpleClient(elastic.SetURL(e.Hosts[prefix].Hosts...), elastic.SetMaxRetries(10)) 34 } else if len(e.Hosts[prefix].Hosts) == 0 { 35 // client option enabled 36 esClients.m[prefix], err = elastic.NewClient(e.Hosts[prefix].ClientOptionFuncs.([]elastic.ClientOptionFunc)...) 37 } else { 38 // default behavior 39 esClients.m[prefix], err = elastic.NewClient(elastic.SetURL(e.Hosts[prefix].Hosts...), elastic.SetMaxRetries(10)) 40 } 41 // esClients.Unlock() 42 if err != nil { 43 return err 44 } 45 return nil 46 } 47 48 // getService returns an elasticsearch service based on the global client 49 func (e *ElasticHosts) getService2(prefix string) (*elastic.SearchService, error) { 50 esClients.Lock() 51 defer esClients.Unlock() 52 53 err := e.InitClient(prefix) 54 if err != nil { 55 return nil, err 56 } 57 return esClients.m[prefix].(*elastic.Client).Search(), nil 58 } 59 60 // Query takes a Logstash request, applies it a search service, and then queries 61 // elasticsearch. 62 func (e ElasticHosts) Query2(r *ElasticRequest2) (*elastic.SearchResult, error) { 63 s, err := e.getService2(r.HostKey) 64 if err != nil { 65 return nil, err 66 } 67 68 s.Index(r.Indices...) 69 70 // With IgnoreUnavailable there can be gaps in the indices (i.e. missing days) and we will not error 71 // If no indices match than there will be no successful shards and and error is returned in that case 72 s.IgnoreUnavailable(true) 73 res, err := s.SearchSource(r.Source).Do() 74 if err != nil { 75 return nil, err 76 } 77 if res.Shards == nil { 78 return nil, fmt.Errorf("no shard info in reply, should not be here please file issue") 79 } 80 if res.Shards.Successful == 0 { 81 return nil, fmt.Errorf("no successful shards in result, perhaps the index does exist, total shards: %v, failed shards: %v", res.Shards.Total, res.Shards.Failed) 82 } 83 return res, nil 84 } 85 86 // ElasticRequest is a container for the information needed to query elasticsearch or a date 87 // histogram. 88 type ElasticRequest2 struct { 89 Indices []string 90 HostKey string 91 Start *time.Time 92 End *time.Time 93 Source *elastic.SearchSource // This the object that we build queries in 94 } 95 96 // CacheKey returns the text of the elastic query. That text is the indentifer for 97 // the query in the cache. It is a combination of the host key, indices queries and the json query content 98 func (r *ElasticRequest2) CacheKey() (string, error) { 99 s, err := r.Source.Source() 100 if err != nil { 101 return "", err 102 } 103 b, err := json.Marshal(s) 104 if err != nil { 105 return "", fmt.Errorf("failed to generate json representation of search source for cache key: %s", s) 106 } 107 108 return fmt.Sprintf("%s:%v\n%s", r.HostKey, r.Indices, b), nil 109 } 110 111 // timeESRequest execute the elasticsearch query (which may set or hit cache) and returns 112 // the search results. 113 func timeESRequest2(e *State, req *ElasticRequest2) (resp *elastic.SearchResult, err error) { 114 var source interface{} 115 source, err = req.Source.Source() 116 if err != nil { 117 return resp, fmt.Errorf("failed to get source of request while timing elastic request: %s", err) 118 } 119 b, err := json.MarshalIndent(source, "", " ") 120 if err != nil { 121 return resp, err 122 } 123 key, err := req.CacheKey() 124 if err != nil { 125 return nil, err 126 } 127 e.Timer.StepCustomTiming("elastic", "query", fmt.Sprintf("%s:%v\n%s", req.HostKey, req.Indices, b), func() { 128 getFn := func() (interface{}, error) { 129 return e.ElasticHosts.Query2(req) 130 } 131 var val interface{} 132 var hit bool 133 val, err, hit = e.Cache.Get(key, getFn) 134 collectCacheHit(e.Cache, "elastic", hit) 135 resp = val.(*elastic.SearchResult) 136 }) 137 return 138 } 139 140 func ESDateHistogram2(prefix string, e *State, indexer ESIndexer, keystring string, filter elastic.Query, interval, sduration, eduration, stat_field, rstat string, size int) (r *Results, err error) { 141 r = new(Results) 142 req, err := ESBaseQuery2(e.now, indexer, filter, sduration, eduration, size, prefix) 143 if err != nil { 144 return nil, err 145 } 146 // Extended bounds and min doc count are required to get values back when the bucket value is 0 147 ts := elastic.NewDateHistogramAggregation().Field(indexer.TimeField).Interval(strings.Replace(interval, "M", "n", -1)).MinDocCount(0).ExtendedBoundsMin(req.Start).ExtendedBoundsMax(req.End).Format(elasticRFC3339) 148 if stat_field != "" { 149 ts = ts.SubAggregation("stats", elastic.NewExtendedStatsAggregation().Field(stat_field)) 150 switch rstat { 151 case "avg", "min", "max", "sum", "sum_of_squares", "variance", "std_deviation": 152 default: 153 return r, fmt.Errorf("stat function %v not a valid option", rstat) 154 } 155 } 156 if keystring == "" { 157 req.Source = req.Source.Aggregation("ts", ts) 158 result, err := timeESRequest2(e, req) 159 if err != nil { 160 return nil, err 161 } 162 ts, found := result.Aggregations.DateHistogram("ts") 163 if !found { 164 return nil, fmt.Errorf("expected time series not found in elastic reply") 165 } 166 series := make(Series) 167 for _, v := range ts.Buckets { 168 val := processESBucketItem2(v, rstat) 169 if val != nil { 170 series[time.Unix(v.Key/1000, 0).UTC()] = *val 171 } 172 } 173 if len(series) == 0 { 174 return r, nil 175 } 176 r.Results = append(r.Results, &Result{ 177 Value: series, 178 Group: make(opentsdb.TagSet), 179 }) 180 return r, nil 181 } 182 keys := strings.Split(keystring, ",") 183 aggregation := elastic.NewTermsAggregation().Field(keys[len(keys)-1]).Size(0) 184 aggregation = aggregation.SubAggregation("ts", ts) 185 for i := len(keys) - 2; i > -1; i-- { 186 aggregation = elastic.NewTermsAggregation().Field(keys[i]).Size(0).SubAggregation("g_"+keys[i+1], aggregation) 187 } 188 req.Source = req.Source.Aggregation("g_"+keys[0], aggregation) 189 result, err := timeESRequest2(e, req) 190 if err != nil { 191 return nil, err 192 } 193 top, ok := result.Aggregations.Terms("g_" + keys[0]) 194 if !ok { 195 return nil, fmt.Errorf("top key g_%v not found in result", keys[0]) 196 } 197 var desc func(*elastic.AggregationBucketKeyItem, opentsdb.TagSet, []string) error 198 desc = func(b *elastic.AggregationBucketKeyItem, tags opentsdb.TagSet, keys []string) error { 199 if ts, found := b.DateHistogram("ts"); found { 200 if e.Squelched(tags) { 201 return nil 202 } 203 series := make(Series) 204 for _, v := range ts.Buckets { 205 val := processESBucketItem2(v, rstat) 206 if val != nil { 207 series[time.Unix(v.Key/1000, 0).UTC()] = *val 208 } 209 } 210 if len(series) == 0 { 211 return nil 212 } 213 r.Results = append(r.Results, &Result{ 214 Value: series, 215 Group: tags.Copy(), 216 }) 217 return nil 218 } 219 if len(keys) < 1 { 220 return nil 221 } 222 n, _ := b.Aggregations.Terms("g_" + keys[0]) 223 for _, item := range n.Buckets { 224 key := fmt.Sprint(item.Key) 225 tags[keys[0]] = key 226 if err := desc(item, tags.Copy(), keys[1:]); err != nil { 227 return err 228 } 229 } 230 return nil 231 } 232 for _, b := range top.Buckets { 233 tags := make(opentsdb.TagSet) 234 key := fmt.Sprint(b.Key) 235 tags[keys[0]] = key 236 if err := desc(b, tags, keys[1:]); err != nil { 237 return nil, err 238 } 239 } 240 return r, nil 241 } 242 243 // ESBaseQuery builds the base query that both ESCount and ESStat share 244 func ESBaseQuery2(now time.Time, indexer ESIndexer, filter elastic.Query, sduration, eduration string, size int, prefix string) (*ElasticRequest2, error) { 245 start, err := opentsdb.ParseDuration(sduration) 246 if err != nil { 247 return nil, err 248 } 249 var end opentsdb.Duration 250 if eduration != "" { 251 end, err = opentsdb.ParseDuration(eduration) 252 if err != nil { 253 return nil, err 254 } 255 } 256 st := now.Add(time.Duration(-start)) 257 en := now.Add(time.Duration(-end)) 258 indices := indexer.Generate(&st, &en) 259 r := ElasticRequest2{ 260 Indices: indices, 261 HostKey: prefix, 262 Start: &st, 263 End: &en, 264 Source: elastic.NewSearchSource().Size(size), 265 } 266 var q elastic.Query 267 q = elastic.NewRangeQuery(indexer.TimeField).Gte(st).Lte(en).Format(elasticRFC3339) 268 r.Source = r.Source.Query(elastic.NewBoolQuery().Must(q, filter)) 269 return &r, nil 270 } 271 272 func ScopeES2(ts opentsdb.TagSet, q elastic.Query) elastic.Query { 273 var filters []elastic.Query 274 for tagKey, tagValue := range ts { 275 filters = append(filters, elastic.NewTermQuery(tagKey, tagValue)) 276 } 277 filters = append(filters, q) 278 b := elastic.NewBoolQuery().Must(filters...) 279 return b 280 } 281 282 func processESBucketItem2(b *elastic.AggregationBucketHistogramItem, rstat string) *float64 { 283 if stats, found := b.ExtendedStats("stats"); found { 284 var val *float64 285 switch rstat { 286 case "avg": 287 val = stats.Avg 288 case "min": 289 val = stats.Min 290 case "max": 291 val = stats.Max 292 case "sum": 293 val = stats.Sum 294 case "sum_of_squares": 295 val = stats.SumOfSquares 296 case "variance": 297 val = stats.Variance 298 case "std_deviation": 299 val = stats.StdDeviation 300 } 301 return val 302 } 303 v := float64(b.DocCount) 304 return &v 305 }