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