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 }