bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/search/search.go (about)

     1  package search // import "bosun.org/cmd/bosun/search"
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"reflect"
     7  	"regexp"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"bosun.org/cmd/bosun/database"
    14  	"bosun.org/collect"
    15  	"bosun.org/metadata"
    16  	"bosun.org/opentsdb"
    17  	"bosun.org/slog"
    18  )
    19  
    20  // Search is a struct to hold indexed data about OpenTSDB metric and tag data.
    21  // It is suited to answering questions about: available metrics for a tag set,
    22  // available tag keys for a metric, and available tag values for a metric and
    23  // tag key.
    24  type Search struct {
    25  	DataAccess database.DataAccess
    26  
    27  	// metric -> tags -> struct
    28  	last map[string]map[string]*database.LastInfo
    29  
    30  	indexQueue chan *opentsdb.DataPoint
    31  	sync.RWMutex
    32  }
    33  
    34  func init() {
    35  	metadata.AddMetricMeta("bosun.search.index_queue", metadata.Gauge, metadata.Count, "Number of datapoints queued for indexing to redis")
    36  	metadata.AddMetricMeta("bosun.search.dropped", metadata.Counter, metadata.Count, "Number of datapoints discarded without being saved to redis")
    37  }
    38  
    39  func NewSearch(data database.DataAccess, skipLast bool) *Search {
    40  	s := Search{
    41  		DataAccess: data,
    42  		last:       make(map[string]map[string]*database.LastInfo),
    43  		indexQueue: make(chan *opentsdb.DataPoint, 300000),
    44  	}
    45  	collect.Set("search.index_queue", opentsdb.TagSet{}, func() interface{} { return len(s.indexQueue) })
    46  	collect.Set("search.last.total", opentsdb.TagSet{}, func() interface{} {
    47  		s.RLock()
    48  		defer s.RUnlock()
    49  		total := 0
    50  		for _, v := range s.last {
    51  			total += len(v)
    52  		}
    53  		return total
    54  	})
    55  	collect.Set("search.last.metrics", opentsdb.TagSet{}, func() interface{} {
    56  		s.RLock()
    57  		defer s.RUnlock()
    58  		return len(s.last)
    59  	})
    60  	if !skipLast {
    61  		s.loadLast()
    62  		go s.redisIndex(s.indexQueue)
    63  		go s.backupLoop()
    64  	}
    65  	return &s
    66  }
    67  
    68  func (s *Search) Index(mdp opentsdb.MultiDataPoint) {
    69  	for _, dp := range mdp {
    70  		s.Lock()
    71  		mmap := s.last[dp.Metric]
    72  		if mmap == nil {
    73  			mmap = make(map[string]*database.LastInfo)
    74  			s.last[dp.Metric] = mmap
    75  		}
    76  		p := mmap[dp.Tags.String()]
    77  		if p == nil {
    78  			p = &database.LastInfo{}
    79  			mmap[dp.Tags.String()] = p
    80  		}
    81  		if p.Timestamp < dp.Timestamp {
    82  			if fv, err := getFloat(dp.Value); err == nil {
    83  				p.DiffFromPrev = (fv - p.LastVal) / float64(dp.Timestamp-p.Timestamp)
    84  				p.LastVal = fv
    85  			} else {
    86  				slog.Error(err)
    87  			}
    88  			p.Timestamp = dp.Timestamp
    89  		}
    90  		s.Unlock()
    91  		select {
    92  		case s.indexQueue <- dp:
    93  		default:
    94  			collect.Add("search.dropped", opentsdb.TagSet{}, 1)
    95  		}
    96  	}
    97  }
    98  
    99  func (s *Search) redisIndex(c <-chan *opentsdb.DataPoint) {
   100  	now := time.Now().Unix()
   101  	nextUpdateTimes := make(map[string]int64)
   102  	updateIfTime := func(key string, f func()) {
   103  		nextUpdate, ok := nextUpdateTimes[key]
   104  		if !ok || now > nextUpdate {
   105  			f()
   106  			nextUpdateTimes[key] = now + int64(30*60+rand.Intn(15*60)) //pick a random time between 30 and 45 minutes from now
   107  		}
   108  	}
   109  	for dp := range c {
   110  		now = time.Now().Unix()
   111  		metric := dp.Metric
   112  		for k, v := range dp.Tags {
   113  			updateIfTime(fmt.Sprintf("kvm:%s:%s:%s", k, v, metric), func() {
   114  				if err := s.DataAccess.Search().AddMetricForTag(k, v, metric, now); err != nil {
   115  					slog.Error(err)
   116  				}
   117  				if err := s.DataAccess.Search().AddTagValue(metric, k, v, now); err != nil {
   118  					slog.Error(err)
   119  				}
   120  			})
   121  			updateIfTime(fmt.Sprintf("mk:%s:%s", metric, k), func() {
   122  				if err := s.DataAccess.Search().AddTagKeyForMetric(metric, k, now); err != nil {
   123  					slog.Error(err)
   124  				}
   125  			})
   126  			updateIfTime(fmt.Sprintf("kv:%s:%s", k, v), func() {
   127  				if err := s.DataAccess.Search().AddTagValue(database.Search_All, k, v, now); err != nil {
   128  					slog.Error(err)
   129  				}
   130  			})
   131  			updateIfTime(fmt.Sprintf("m:%s", metric), func() {
   132  				if err := s.DataAccess.Search().AddMetric(metric, now); err != nil {
   133  					slog.Error(err)
   134  				}
   135  			})
   136  		}
   137  		updateIfTime(fmt.Sprintf("mts:%s:%s", metric, dp.Tags.Tags()), func() {
   138  			if err := s.DataAccess.Search().AddMetricTagSet(metric, dp.Tags.Tags(), now); err != nil {
   139  				slog.Error(err)
   140  			}
   141  		})
   142  	}
   143  }
   144  
   145  var floatType = reflect.TypeOf(float64(0))
   146  
   147  func getFloat(unk interface{}) (float64, error) {
   148  	v := reflect.ValueOf(unk)
   149  	v = reflect.Indirect(v)
   150  	if !v.Type().ConvertibleTo(floatType) {
   151  		return 0, fmt.Errorf("cannot convert %v to float64", v.Type())
   152  	}
   153  	fv := v.Convert(floatType)
   154  	return fv.Float(), nil
   155  }
   156  
   157  // Match returns all matching values against search. search is a regex, except
   158  // that `.` is literal, `*` can be used for `.*`, and the entire string is
   159  // searched (`^` and `&` added to ends of search).
   160  func Match(search string, values []string) ([]string, error) {
   161  	v := strings.Replace(search, ".", `\.`, -1)
   162  	v = strings.Replace(v, "*", ".*", -1)
   163  	v = "^" + v + "$"
   164  	re, err := regexp.Compile(v)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	var nvs []string
   169  	for _, nv := range values {
   170  		if re.MatchString(nv) {
   171  			nvs = append(nvs, nv)
   172  		}
   173  	}
   174  	return nvs, nil
   175  }
   176  
   177  var errNotFloat = fmt.Errorf("last: expected float64")
   178  
   179  // GetLast returns the value of the most recent data point for the given metric
   180  // and tag. tags should be of the form "{key=val,key2=val2}". If diff is true,
   181  // the value is treated as a counter. err is non nil if there is no match.
   182  func (s *Search) GetLast(metric, tags string, diff bool) (v float64, t int64, err error) {
   183  	s.RLock()
   184  	defer s.RUnlock()
   185  	m, mOk := s.last[metric]
   186  	if mOk {
   187  		p := m[tags]
   188  		if p != nil {
   189  			if diff {
   190  				return p.DiffFromPrev, p.Timestamp, nil
   191  			}
   192  			return p.LastVal, p.Timestamp, nil
   193  		}
   194  	}
   195  	return 0, 0, fmt.Errorf("no match for %s:%s", metric, tags)
   196  }
   197  
   198  // GetLastInt64 is like GetLast but converts the value to an int64
   199  func (s *Search) GetLastInt64(metric, tags string, diff bool) (int64, int64, error) {
   200  	v, t, err := s.GetLast(metric, tags, diff)
   201  	return int64(v), t, err
   202  }
   203  
   204  // load stored last data from redis
   205  func (s *Search) loadLast() {
   206  	s.Lock()
   207  	defer s.Unlock()
   208  	slog.Info("Loading last datapoints from redis")
   209  	m, err := s.DataAccess.Search().LoadLastInfos()
   210  	if err != nil {
   211  		slog.Error(err)
   212  	} else {
   213  		s.last = m
   214  	}
   215  	slog.Info("Done")
   216  }
   217  
   218  func (s *Search) backupLoop() {
   219  	for {
   220  		time.Sleep(2 * time.Minute)
   221  		slog.Info("Backing up last data to redis")
   222  		err := s.BackupLast()
   223  		if err != nil {
   224  			slog.Error(err)
   225  		}
   226  	}
   227  }
   228  
   229  func (s *Search) BackupLast() error {
   230  	s.RLock()
   231  	copyL := make(map[string]map[string]*database.LastInfo, len(s.last))
   232  	for m, mmap := range s.last {
   233  		innerCopy := make(map[string]*database.LastInfo, len(mmap))
   234  		copyL[m] = innerCopy
   235  		for ts, info := range mmap {
   236  			innerCopy[ts] = &database.LastInfo{
   237  				LastVal:      info.LastVal,
   238  				DiffFromPrev: info.DiffFromPrev,
   239  				Timestamp:    info.Timestamp,
   240  			}
   241  		}
   242  	}
   243  	s.RUnlock()
   244  	return s.DataAccess.Search().BackupLastInfos(copyL)
   245  }
   246  
   247  func (s *Search) Expand(q *opentsdb.Query) error {
   248  	for k, ov := range q.Tags {
   249  		var nvs []string
   250  		for _, v := range strings.Split(ov, "|") {
   251  			v = strings.TrimSpace(v)
   252  			if v == "*" || !strings.Contains(v, "*") {
   253  				nvs = append(nvs, v)
   254  			} else {
   255  				vs, err := s.TagValuesByMetricTagKey(q.Metric, k, 0)
   256  				if err != nil {
   257  					return err
   258  				}
   259  				ns, err := Match(v, vs)
   260  				if err != nil {
   261  					return err
   262  				}
   263  				nvs = append(nvs, ns...)
   264  			}
   265  		}
   266  		if len(nvs) == 0 {
   267  			return fmt.Errorf("expr: no tags matching %s=%s", k, ov)
   268  		}
   269  		q.Tags[k] = strings.Join(nvs, "|")
   270  	}
   271  	return nil
   272  }
   273  
   274  // UniqueMetrics returns a sorted slice of metrics where the
   275  // metric has been updated more recently than epoch
   276  func (s *Search) UniqueMetrics(epochFilter int64) ([]string, error) {
   277  	m, err := s.DataAccess.Search().GetAllMetrics()
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	metrics := []string{}
   282  	for k, epoch := range m {
   283  		if epoch < epochFilter {
   284  			continue
   285  		}
   286  		metrics = append(metrics, k)
   287  	}
   288  	sort.Strings(metrics)
   289  	return metrics, nil
   290  }
   291  
   292  func (s *Search) TagValuesByTagKey(Tagk string, since time.Duration) ([]string, error) {
   293  	return s.TagValuesByMetricTagKey(database.Search_All, Tagk, since)
   294  }
   295  
   296  func (s *Search) MetricsByTagPair(tagk, tagv string, since time.Duration) ([]string, error) {
   297  	var t int64
   298  	if since > 0 {
   299  		t = time.Now().Add(-since).Unix()
   300  	}
   301  	metrics, err := s.DataAccess.Search().GetMetricsForTag(tagk, tagv)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	r := []string{}
   306  	for k, ts := range metrics {
   307  		if t <= ts {
   308  			r = append(r, k)
   309  		}
   310  	}
   311  	sort.Strings(r)
   312  	return r, nil
   313  }
   314  
   315  func (s *Search) TagKeysByMetric(metric string) ([]string, error) {
   316  	keys, err := s.DataAccess.Search().GetTagKeysForMetric(metric)
   317  	if err != nil {
   318  		return nil, err
   319  	}
   320  	r := []string{}
   321  	for k := range keys {
   322  		r = append(r, k)
   323  	}
   324  	sort.Strings(r)
   325  	return r, nil
   326  }
   327  
   328  func (s *Search) TagValuesByMetricTagKey(metric, tagK string, since time.Duration) ([]string, error) {
   329  	var t int64
   330  	if since > 0 {
   331  		t = time.Now().Add(-since).Unix()
   332  	}
   333  	vals, err := s.DataAccess.Search().GetTagValues(metric, tagK)
   334  	if err != nil {
   335  		return nil, err
   336  	}
   337  	r := []string{}
   338  	for k, ts := range vals {
   339  		if t <= ts {
   340  			r = append(r, k)
   341  		}
   342  	}
   343  	sort.Strings(r)
   344  	return r, nil
   345  }
   346  
   347  func (s *Search) FilteredTagSets(metric string, tags opentsdb.TagSet, since int64) ([]opentsdb.TagSet, error) {
   348  	sets, err := s.DataAccess.Search().GetMetricTagSets(metric, tags)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  	r := []opentsdb.TagSet{}
   353  	for k, lastSeen := range sets {
   354  		ts, err := opentsdb.ParseTags(k)
   355  		if err != nil {
   356  			return nil, err
   357  		}
   358  		if lastSeen >= since {
   359  			r = append(r, ts)
   360  		}
   361  
   362  	}
   363  	return r, nil
   364  }