1package v2 2 3import ( 4 "errors" 5 fmt "fmt" 6 "net/url" 7 "path" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 stringsutil "github.com/sensu/sensu-go/util/strings" 14) 15 16const ( 17 // EventsResource is the name of this resource type 18 EventsResource = "events" 19 20 // EventFailingState indicates failing check result status 21 EventFailingState = "failing" 22 23 // EventFlappingState indicates a rapid change in check result status 24 EventFlappingState = "flapping" 25 26 // EventPassingState indicates successful check result status 27 EventPassingState = "passing" 28) 29 30// StorePrefix returns the path prefix to this resource in the store 31func (e *Event) StorePrefix() string { 32 return EventsResource 33} 34 35// URIPath returns the path component of an event URI. 36func (e *Event) URIPath() string { 37 if !e.HasCheck() { 38 return "" 39 } 40 return path.Join(URLPrefix, "namespaces", url.PathEscape(e.Entity.Namespace), EventsResource, url.PathEscape(e.Entity.Name), url.PathEscape(e.Check.Name)) 41} 42 43// Validate returns an error if the event does not pass validation tests. 44func (e *Event) Validate() error { 45 if e.Entity == nil { 46 return errors.New("event must contain an entity") 47 } 48 49 if !e.HasCheck() && !e.HasMetrics() { 50 return errors.New("event must contain a check or metrics") 51 } 52 53 if err := e.Entity.Validate(); err != nil { 54 return errors.New("entity is invalid: " + err.Error()) 55 } 56 57 if e.HasCheck() { 58 if err := e.Check.Validate(); err != nil { 59 return errors.New("check is invalid: " + err.Error()) 60 } 61 } 62 63 if e.HasMetrics() { 64 if err := e.Metrics.Validate(); err != nil { 65 return errors.New("metrics are invalid: " + err.Error()) 66 } 67 } 68 69 if e.Name != "" { 70 return errors.New("events cannot be named") 71 } 72 73 return nil 74} 75 76// HasCheck determines if an event has check data. 77func (e *Event) HasCheck() bool { 78 return e.Check != nil 79} 80 81// HasMetrics determines if an event has metric data. 82func (e *Event) HasMetrics() bool { 83 return e.Metrics != nil 84} 85 86// IsIncident determines if an event indicates an incident. 87func (e *Event) IsIncident() bool { 88 return e.HasCheck() && e.Check.Status != 0 89} 90 91// IsResolution returns true if an event has just transitionned from an incident 92func (e *Event) IsResolution() bool { 93 if !e.HasCheck() { 94 return false 95 } 96 97 // Try to retrieve the previous status in the check history and verify if it 98 // was a non-zero status, therefore indicating a resolution. The current event 99 // has already been added to the check history by eventd so we must retrieve 100 // the second to the last 101 return (len(e.Check.History) > 1 && 102 e.Check.History[len(e.Check.History)-2].Status != 0 && 103 !e.IsIncident()) 104} 105 106// IsSilenced determines if an event has any silenced entries 107func (e *Event) IsSilenced() bool { 108 if !e.HasCheck() { 109 return false 110 } 111 112 return len(e.Check.Silenced) > 0 113} 114 115// SynthesizeExtras implements dynamic.SynthesizeExtras 116func (e *Event) SynthesizeExtras() map[string]interface{} { 117 return map[string]interface{}{ 118 "has_check": e.HasCheck(), 119 "has_metrics": e.HasMetrics(), 120 "is_incident": e.IsIncident(), 121 "is_resolution": e.IsResolution(), 122 "is_silenced": e.IsSilenced(), 123 } 124} 125 126// FixtureEvent returns a testing fixture for an Event object. 127func FixtureEvent(entityName, checkID string) *Event { 128 return &Event{ 129 ObjectMeta: NewObjectMeta("", "default"), 130 Timestamp: time.Now().Unix(), 131 Entity: FixtureEntity(entityName), 132 Check: FixtureCheck(checkID), 133 } 134} 135 136// NewEvent creates a new Event. 137func NewEvent(meta ObjectMeta) *Event { 138 return &Event{ObjectMeta: meta} 139} 140 141// 142// Sorting 143 144// EventsBySeverity can be used to sort a given collection of events by check 145// status and timestamp. 146func EventsBySeverity(es []*Event) sort.Interface { 147 return &eventSorter{es, createCmpEvents( 148 cmpBySeverity, 149 cmpByLastOk, 150 cmpByUniqueComponents, 151 )} 152} 153 154// EventsByTimestamp can be used to sort a given collection of events by time it 155// occurred. 156func EventsByTimestamp(es []*Event, asc bool) sort.Interface { 157 sorter := &eventSorter{events: es} 158 if asc { 159 sorter.byFn = func(a, b *Event) bool { 160 return a.Timestamp > b.Timestamp 161 } 162 } else { 163 sorter.byFn = func(a, b *Event) bool { 164 return a.Timestamp < b.Timestamp 165 } 166 } 167 return sorter 168} 169 170// EventsByLastOk can be used to sort a given collection of events by time it 171// last received an OK status. 172func EventsByLastOk(es []*Event) sort.Interface { 173 return &eventSorter{es, createCmpEvents( 174 cmpByIncident, 175 cmpByLastOk, 176 cmpByUniqueComponents, 177 )} 178} 179 180func cmpByUniqueComponents(a, b *Event) int { 181 ai, bi := "", "" 182 if a.Entity != nil { 183 ai += a.Entity.Name 184 } 185 if a.Check != nil { 186 ai += a.Check.Name 187 } 188 if b.Entity != nil { 189 bi = b.Entity.Name 190 } 191 if b.Check != nil { 192 bi += b.Check.Name 193 } 194 195 if ai == bi { 196 return 0 197 } else if ai < bi { 198 return 1 199 } 200 return -1 201} 202 203func cmpBySeverity(a, b *Event) int { 204 ap, bp := deriveSeverity(a), deriveSeverity(b) 205 206 // Sort events with the same exit status by timestamp 207 if ap == bp { 208 return 0 209 } else if ap < bp { 210 return 1 211 } 212 return -1 213} 214 215func cmpByIncident(a, b *Event) int { 216 av, bv := a.IsIncident(), b.IsIncident() 217 218 // Rank higher if incident 219 if av == bv { 220 return 0 221 } else if av { 222 return 1 223 } 224 return -1 225} 226 227func cmpByLastOk(a, b *Event) int { 228 at, bt := a.Timestamp, b.Timestamp 229 if a.HasCheck() { 230 at = a.Check.LastOK 231 } 232 if b.HasCheck() { 233 bt = b.Check.LastOK 234 } 235 236 if at == bt { 237 return 0 238 } else if at > bt { 239 return 1 240 } 241 return -1 242} 243 244// Based on convention we define the order of importance as critical (2), 245// warning (1), unknown (>2), and Ok (0). If event is not a check sort to 246// very end. 247func deriveSeverity(e *Event) int { 248 if e.HasCheck() { 249 switch e.Check.Status { 250 case 0: 251 return 3 252 case 1: 253 return 1 254 case 2: 255 return 0 256 default: 257 return 2 258 } 259 } 260 return 4 261} 262 263type cmpEvents func(a, b *Event) int 264 265func createCmpEvents(cmps ...cmpEvents) func(a, b *Event) bool { 266 return func(a, b *Event) bool { 267 for _, cmp := range cmps { 268 st := cmp(a, b) 269 if st == 0 { // if equal try the next comparitor 270 continue 271 } 272 return st == 1 273 } 274 return true 275 } 276} 277 278type eventSorter struct { 279 events []*Event 280 byFn func(a, b *Event) bool 281} 282 283// Len implements sort.Interface. 284func (s *eventSorter) Len() int { 285 return len(s.events) 286} 287 288// Swap implements sort.Interface. 289func (s *eventSorter) Swap(i, j int) { 290 s.events[i], s.events[j] = s.events[j], s.events[i] 291} 292 293// Less implements sort.Interface. 294func (s *eventSorter) Less(i, j int) bool { 295 return s.byFn(s.events[i], s.events[j]) 296} 297 298// SilencedBy returns the subset of given silences, that silence the event. 299func (e *Event) SilencedBy(entries []*Silenced) []*Silenced { 300 silencedBy := make([]*Silenced, 0, len(entries)) 301 if !e.HasCheck() { 302 return silencedBy 303 } 304 305 // Loop through every silenced entries in order to determine if it applies to 306 // the given event 307 for _, entry := range entries { 308 if e.IsSilencedBy(entry) { 309 silencedBy = append(silencedBy, entry) 310 } 311 } 312 313 return silencedBy 314} 315 316// IsSilencedBy returns true if given silence will silence the event. 317func (e *Event) IsSilencedBy(entry *Silenced) bool { 318 if !e.HasCheck() { 319 return false 320 } 321 322 // Make sure the silence has started 323 now := time.Now().Unix() 324 if !entry.StartSilence(now) { 325 return false 326 } 327 328 // Is this event silenced for all subscriptions? (e.g. *:check_cpu) 329 if entry.Name == fmt.Sprintf("*:%s", e.Check.Name) { 330 return true 331 } 332 333 // Is this event silenced by the entity subscription? (e.g. entity:id:*) 334 if entry.Name == fmt.Sprintf("%s:*", GetEntitySubscription(e.Entity.Name)) { 335 return true 336 } 337 338 // Is this event silenced for this particular entity? (e.g. 339 // entity:id:check_cpu) 340 if entry.Name == fmt.Sprintf("%s:%s", GetEntitySubscription(e.Entity.Name), e.Check.Name) { 341 return true 342 } 343 344 for _, subscription := range e.Check.Subscriptions { 345 // Make sure the entity is subscribed to this specific subscription 346 if !stringsutil.InArray(subscription, e.Entity.Subscriptions) { 347 continue 348 } 349 350 // Is this event silenced by one of the check subscription? (e.g. 351 // load-balancer:*) 352 if entry.Name == fmt.Sprintf("%s:*", subscription) { 353 return true 354 } 355 356 // Is this event silenced by one of the check subscription for this 357 // particular check? (e.g. load-balancer:check_cpu) 358 if entry.Name == fmt.Sprintf("%s:%s", subscription, e.Check.Name) { 359 return true 360 } 361 } 362 363 return false 364} 365 366// EventFields returns a set of fields that represent that resource 367func EventFields(r Resource) map[string]string { 368 resource := r.(*Event) 369 return map[string]string{ 370 "event.name": resource.ObjectMeta.Name, 371 "event.namespace": resource.ObjectMeta.Namespace, 372 "event.check.handlers": strings.Join(resource.Check.Handlers, ","), 373 "event.check.publish": strconv.FormatBool(resource.Check.Publish), 374 "event.check.round_robin": strconv.FormatBool(resource.Check.RoundRobin), 375 "event.check.runtime_assets": strings.Join(resource.Check.RuntimeAssets, ","), 376 "event.check.status": strconv.Itoa(int(resource.Check.Status)), 377 "event.check.subscriptions": strings.Join(resource.Check.Subscriptions, ","), 378 "event.entity.deregister": strconv.FormatBool(resource.Entity.Deregister), 379 "event.entity.entity_class": resource.Entity.EntityClass, 380 "event.entity.subscriptions": strings.Join(resource.Entity.Subscriptions, ","), 381 } 382} 383 384// SetNamespace sets the namespace of the resource. 385func (e *Event) SetNamespace(namespace string) { 386 e.Namespace = namespace 387} 388