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

     1  package expr
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"math"
    10  
    11  	"bosun.org/annotate"
    12  	"bosun.org/cmd/bosun/expr/parse"
    13  	"bosun.org/models"
    14  	"bosun.org/opentsdb"
    15  	"github.com/kylebrandt/boolq"
    16  )
    17  
    18  var Annotate = map[string]parse.Func{
    19  	// Funcs for querying elastic
    20  	"ancounts": {
    21  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
    22  		Return: models.TypeSeriesSet,
    23  		Tags:   tagFirst,
    24  		F:      AnCounts,
    25  	},
    26  	"andurations": {
    27  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString},
    28  		Return: models.TypeSeriesSet,
    29  		Tags:   tagFirst,
    30  		F:      AnDurations,
    31  	},
    32  	"antable": {
    33  		Args:   []models.FuncType{models.TypeString, models.TypeString, models.TypeString, models.TypeString},
    34  		Return: models.TypeTable,
    35  		F:      AnTable,
    36  	},
    37  }
    38  
    39  func procDuration(e *State, startDuration, endDuration string) (time.Time, time.Time, error) {
    40  	start, err := opentsdb.ParseDuration(startDuration)
    41  	if err != nil {
    42  		return time.Time{}, time.Time{}, err
    43  	}
    44  	var end opentsdb.Duration
    45  	if endDuration != "" {
    46  		end, err = opentsdb.ParseDuration(endDuration)
    47  		if err != nil {
    48  			return time.Time{}, time.Time{}, err
    49  		}
    50  	}
    51  	st := e.now.Add(time.Duration(-start))
    52  	en := e.now.Add(time.Duration(-end))
    53  	return st, en, nil
    54  }
    55  
    56  func getAndFilterAnnotations(e *State, start, end time.Time, filter string) (annotate.Annotations, error) {
    57  	annotations, err := e.Annotate.GetAnnotations(&start, &end)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	var t *boolq.Tree
    62  	if filter != "" {
    63  		var err error
    64  		t, err = boolq.Parse(filter)
    65  		if err != nil {
    66  			return nil, fmt.Errorf("failed to parse annotation filter: %v", err)
    67  		}
    68  	}
    69  	filteredAnnotations := annotate.Annotations{}
    70  	for _, a := range annotations {
    71  		if filter == "" {
    72  			filteredAnnotations = append(filteredAnnotations, a)
    73  			continue
    74  		}
    75  		match, err := boolq.AskParsedExpr(t, a)
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  		if match {
    80  			filteredAnnotations = append(filteredAnnotations, a)
    81  		}
    82  	}
    83  	sort.Sort(sort.Reverse(annotate.AnnotationsByStartID(filteredAnnotations)))
    84  	return filteredAnnotations, nil
    85  }
    86  
    87  func AnDurations(e *State, filter, startDuration, endDuration string) (r *Results, err error) {
    88  	reqStart, reqEnd, err := procDuration(e, startDuration, endDuration)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	filteredAnnotations, err := getAndFilterAnnotations(e, reqStart, reqEnd, filter)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	series := make(Series)
    97  	for i, a := range filteredAnnotations {
    98  		aStart := a.StartDate.Time
    99  		aEnd := a.EndDate.Time
   100  		inBounds := (aStart.After(reqStart) || aStart == reqStart) && (aEnd.Before(reqEnd) || aEnd == reqEnd)
   101  		entirelyOutOfBounds := aStart.Before(reqStart) && aEnd.After(reqEnd)
   102  		aDuration := aEnd.Sub(aStart)
   103  		if inBounds {
   104  			// time has no meaning here, so we just make the key an index since we don't have an array type
   105  			series[time.Unix(int64(i), 0).UTC()] = aDuration.Seconds()
   106  		} else if entirelyOutOfBounds {
   107  			// Duration is equal to that of the full request
   108  			series[time.Unix(int64(i), 0).UTC()] = reqEnd.Sub(reqStart).Seconds()
   109  		} else if aDuration == 0 {
   110  			// This would mean an out of bounds. Should never be here, but if we don't return an error in the case that we do end up here then we might panic on divide by zero later in the code
   111  			return nil, fmt.Errorf("unexpected annotation with 0 duration outside of request bounds (please file an issue)")
   112  		} else if aStart.Before(reqStart) {
   113  			aDurationAfterReqStart := aEnd.Sub(reqStart)
   114  			series[time.Unix(int64(i), 0).UTC()] = aDurationAfterReqStart.Seconds()
   115  			continue
   116  		} else if aEnd.After(reqEnd) {
   117  			aDurationBeforeReqEnd := reqEnd.Sub(aStart)
   118  			series[time.Unix(int64(i), 0).UTC()] = aDurationBeforeReqEnd.Seconds()
   119  		}
   120  	}
   121  	if len(series) == 0 {
   122  		series[time.Unix(0, 0).UTC()] = math.NaN()
   123  	}
   124  	return &Results{
   125  		Results: []*Result{
   126  			{Value: series},
   127  		},
   128  	}, nil
   129  }
   130  
   131  func AnCounts(e *State, filter, startDuration, endDuration string) (r *Results, err error) {
   132  	reqStart, reqEnd, err := procDuration(e, startDuration, endDuration)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	filteredAnnotations, err := getAndFilterAnnotations(e, reqStart, reqEnd, filter)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	series := make(Series)
   141  	for i, a := range filteredAnnotations {
   142  		aStart := a.StartDate.Time
   143  		aEnd := a.EndDate.Time
   144  		aDuration := aEnd.Sub(aStart)
   145  		inBounds := (aStart.After(reqStart) || aStart == reqStart) && (aEnd.Before(reqEnd) || aEnd == reqEnd)
   146  		entirelyOutOfBounds := aStart.Before(reqStart) && aEnd.After(reqEnd)
   147  		if inBounds || entirelyOutOfBounds {
   148  			// time has no meaning here, so we just make the key an index since we don't have an array type
   149  			series[time.Unix(int64(i), 0).UTC()] = 1
   150  			continue
   151  		} else if aDuration == 0 {
   152  			// This would mean an out of bounds. Should never be here, but if we don't return an error in the case that we do end up here then we might panic on divide by zero later in the code
   153  			return nil, fmt.Errorf("unexpected annotation with 0 duration outside of request bounds (please file an issue)")
   154  		} else if aStart.Before(reqStart) {
   155  			aDurationAfterReqStart := aEnd.Sub(reqStart)
   156  			percentBeforeStart := float64(aDurationAfterReqStart) / float64(aDuration)
   157  			series[time.Unix(int64(i), 0).UTC()] = percentBeforeStart
   158  			continue
   159  		} else if aEnd.After(reqEnd) {
   160  			aDurationBeforeReqEnd := reqEnd.Sub(aStart)
   161  			percentAfterEnd := float64(aDurationBeforeReqEnd) / float64(aDuration)
   162  			series[time.Unix(int64(i), 0).UTC()] = percentAfterEnd
   163  		}
   164  	}
   165  	if len(series) == 0 {
   166  		series[time.Unix(0, 0).UTC()] = math.NaN()
   167  	}
   168  	return &Results{
   169  		Results: []*Result{
   170  			{Value: series},
   171  		},
   172  	}, nil
   173  }
   174  
   175  // AnTable returns a table response (meant for Grafana) of matching annotations based on the requested fields
   176  func AnTable(e *State, filter, fieldsCSV, startDuration, endDuration string) (r *Results, err error) {
   177  	start, end, err := procDuration(e, startDuration, endDuration)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	columns := strings.Split(fieldsCSV, ",")
   182  	columnLen := len(columns)
   183  	if columnLen == 0 {
   184  		return nil, fmt.Errorf("must specify at least one column")
   185  	}
   186  	columnIndex := make(map[string]int, columnLen)
   187  	for i, v := range columns {
   188  		// switch is so we fail before fetching annotations
   189  		switch v {
   190  		case "start", "end", "owner", "user", "host", "category", "url", "message", "duration", "link":
   191  			// Pass
   192  		default:
   193  			return nil, fmt.Errorf("%v is not a valid column, must be start, end, owner, user, host, category, url, link, or message", v)
   194  		}
   195  		columnIndex[v] = i
   196  	}
   197  	filteredAnnotations, err := getAndFilterAnnotations(e, start, end, filter)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	t := Table{Columns: columns}
   202  	for _, a := range filteredAnnotations {
   203  		row := make([]interface{}, columnLen)
   204  		for _, c := range columns {
   205  			switch c {
   206  			case "start":
   207  				row[columnIndex["start"]] = a.StartDate
   208  			case "end":
   209  				row[columnIndex["end"]] = a.EndDate
   210  			case "owner":
   211  				row[columnIndex["owner"]] = a.Owner
   212  			case "user":
   213  				row[columnIndex["user"]] = a.CreationUser
   214  			case "host":
   215  				row[columnIndex["host"]] = a.Host
   216  			case "category":
   217  				row[columnIndex["category"]] = a.Category
   218  			case "url":
   219  				row[columnIndex["url"]] = a.Url
   220  			case "message":
   221  				row[columnIndex["message"]] = a.Message
   222  			case "link":
   223  				if a.Url == "" {
   224  					row[columnIndex["link"]] = ""
   225  					continue
   226  				}
   227  				short := a.Url
   228  				if len(short) > 40 {
   229  					short = short[:40]
   230  				}
   231  				row[columnIndex["link"]] = fmt.Sprintf(`<a href="%v" target="_blank">%v</a>`, a.Url, short)
   232  			case "duration":
   233  				d := a.EndDate.Sub(a.StartDate.Time)
   234  				// Format Time in a way that can be lexically sorted
   235  				row[columnIndex["duration"]] = hhhmmss(d)
   236  			}
   237  		}
   238  		t.Rows = append(t.Rows, row)
   239  	}
   240  	return &Results{
   241  		Results: []*Result{
   242  			{Value: t},
   243  		},
   244  	}, nil
   245  }
   246  
   247  // hhmmss formats a duration into HHH:MM:SS (Hours, Minutes, Seconds) so it can be lexically sorted
   248  // up to 999 hours
   249  func hhhmmss(d time.Duration) string {
   250  	hours := int64(d.Hours())
   251  	minutes := int64((d - time.Duration(time.Duration(hours)*time.Hour)).Minutes())
   252  	seconds := int64((d - time.Duration(time.Duration(minutes)*time.Minute)).Seconds())
   253  	return fmt.Sprintf("%03d:%02d:%02d", hours, minutes, seconds)
   254  }