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