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  }