Skip to content

Commit f3ad7bd

Browse files
yhabteabjulianbrost
authored andcommitted
Ignore flapping state if flapping detection isn't enabled
1 parent f0e9515 commit f3ad7bd

File tree

4 files changed

+87
-10
lines changed

4 files changed

+87
-10
lines changed

internal/icinga2/api_responses.go

+9
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ type HostServiceRuntimeAttributes struct {
161161
Acknowledgement int `json:"acknowledgement"`
162162
IsFlapping bool `json:"flapping"`
163163
AcknowledgementLastChange UnixFloat `json:"acknowledgement_last_change"`
164+
EnableFlapping bool `json:"enable_flapping"`
164165
}
165166

166167
// MarshalLogObject implements the zapcore.ObjectMarshaler interface.
@@ -352,6 +353,14 @@ type ObjectCreatedDeleted struct {
352353
EventType string `json:"type"`
353354
}
354355

356+
// IcingaApplication represents the Icinga 2 API status endpoint query result of type IcingaApplication.
357+
// https://icinga.com/docs/icinga-2/latest/doc/12-icinga2-api/#status-and-statistics
358+
type IcingaApplication struct {
359+
App struct {
360+
EnableFlapping bool `json:"enable_flapping"`
361+
} `json:"app"`
362+
}
363+
355364
// UnmarshalEventStreamResponse unmarshal a JSON response line from the Icinga 2 API Event Stream.
356365
//
357366
// The function expects an Icinga 2 API Event Stream Response in its JSON form and tries to unmarshal it into one of the

internal/icinga2/client.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,9 @@ func (client *Client) buildAcknowledgementEvent(ctx context.Context, ack *Acknow
265265
if err != nil {
266266
return nil, err
267267
}
268-
if !isMuted(queryResult) {
268+
if muted, err := isMuted(ctx, client, queryResult); err != nil {
269+
return nil, err
270+
} else if !muted {
269271
ev.Message = queryResult.Attrs.LastCheckResult.Output
270272
ev.SetMute(false, "Acknowledgement cleared")
271273
}
@@ -310,7 +312,9 @@ func (client *Client) buildDowntimeEvent(ctx context.Context, d Downtime, startE
310312
if err != nil {
311313
return nil, err
312314
}
313-
if !isMuted(queryResult) {
315+
if muted, err := isMuted(ctx, client, queryResult); err != nil {
316+
return nil, err
317+
} else if !muted {
314318
// When a downtime is cancelled/expired and there's no other active downtime/ack, we're going to send some
315319
// notifications if there's still an active incident. Therefore, we need the most recent CheckResult of
316320
// that Checkable to use it for the notifications.
@@ -347,7 +351,9 @@ func (client *Client) buildFlappingEvent(ctx context.Context, flapping *Flapping
347351
if err != nil {
348352
return nil, err
349353
}
350-
if !isMuted(queryResult) {
354+
if muted, err := isMuted(ctx, client, queryResult); err != nil {
355+
return nil, err
356+
} else if !muted {
351357
ev.Message = queryResult.Attrs.LastCheckResult.Output
352358
ev.SetMute(false, reason)
353359
}

internal/icinga2/client_api.go

+48-5
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,44 @@ func (client *Client) queryObjectsApiQuery(ctx context.Context, objType string,
149149
})
150150
}
151151

152+
// fetchIcingaAppStatus retrieves the global state of the IcingaApplication type via the /v1/status endpoint.
153+
func (client *Client) fetchIcingaAppStatus(ctx context.Context) (*IcingaApplication, error) {
154+
response, err := client.queryObjectsApi(
155+
ctx,
156+
[]string{"/v1/status/IcingaApplication/"},
157+
http.MethodGet,
158+
nil,
159+
map[string]string{"Accept": "application/json"})
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
defer func() {
165+
_, _ = io.Copy(io.Discard, response)
166+
_ = response.Close()
167+
}()
168+
169+
type status struct {
170+
Status struct {
171+
IcingaApplication *IcingaApplication `json:"icingaapplication"`
172+
} `json:"status"`
173+
}
174+
175+
var results []status
176+
err = json.NewDecoder(response).Decode(&struct {
177+
Results *[]status `json:"results"`
178+
}{&results})
179+
if err != nil {
180+
return nil, err
181+
}
182+
183+
if len(results) == 0 {
184+
return nil, fmt.Errorf("unable to fetch IcingaApplication status")
185+
}
186+
187+
return results[0].Status.IcingaApplication, nil
188+
}
189+
152190
// fetchCheckable fetches the Checkable config state of the given Host/Service name from the Icinga 2 API.
153191
func (client *Client) fetchCheckable(ctx context.Context, host, service string) (*ObjectQueriesResult[HostServiceRuntimeAttributes], error) {
154192
objType, objName := "host", host
@@ -260,8 +298,13 @@ func (client *Client) checkMissedChanges(ctx context.Context, objType string, ca
260298
}
261299

262300
attrs := objQueriesResult.Attrs
301+
checkableIsMuted, err := isMuted(ctx, client, &objQueriesResult)
302+
if err != nil {
303+
return err
304+
}
305+
263306
var fakeEv *event.Event
264-
if attrs.Acknowledgement != AcknowledgementNone {
307+
if checkableIsMuted && attrs.Acknowledgement != AcknowledgementNone {
265308
ackComment, err := client.fetchAcknowledgementComment(ctx, hostName, serviceName, attrs.AcknowledgementLastChange.Time())
266309
if err != nil {
267310
return fmt.Errorf("fetching acknowledgement comment for %q failed, %w", objectName, err)
@@ -275,17 +318,17 @@ func (client *Client) checkMissedChanges(ctx context.Context, objType string, ca
275318
if err != nil {
276319
return fmt.Errorf("failed to construct Event from Acknowledgement response, %w", err)
277320
}
278-
} else if isMuted(&objQueriesResult) {
321+
} else if checkableIsMuted {
279322
fakeEv, err = client.buildCommonEvent(ctx, hostName, serviceName)
280323
if err != nil {
281324
return fmt.Errorf("failed to construct checkable fake mute event: %w", err)
282325
}
283326

284327
fakeEv.Type = event.TypeMute
285-
if attrs.IsFlapping {
286-
fakeEv.SetMute(true, "Checkable is flapping, but we missed the Icinga 2 FlappingStart event")
287-
} else {
328+
if attrs.DowntimeDepth != 0 {
288329
fakeEv.SetMute(true, "Checkable is in downtime, but we missed the Icinga 2 DowntimeStart event")
330+
} else {
331+
fakeEv.SetMute(true, "Checkable is flapping, but we missed the Icinga 2 FlappingStart event")
289332
}
290333
} else {
291334
// This could potentially produce numerous superfluous database (event table) entries if we generate such

internal/icinga2/util.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package icinga2
22

33
import (
4+
"context"
45
"net/url"
56
"strings"
67
)
@@ -18,6 +19,24 @@ func rawurlencode(s string) string {
1819
}
1920

2021
// isMuted returns true if the given checkable is either in Downtime, Flapping or acknowledged, otherwise false.
21-
func isMuted(checkable *ObjectQueriesResult[HostServiceRuntimeAttributes]) bool {
22-
return checkable.Attrs.IsFlapping || checkable.Attrs.Acknowledgement != AcknowledgementNone || checkable.Attrs.DowntimeDepth != 0
22+
//
23+
// When the checkable is Flapping, and neither the flapping detection for that Checkable nor for the entire zone is
24+
// enabled, this will always return false.
25+
//
26+
// Returns an error if it fails to query the status of IcingaApplication from the /v1/status endpoint.
27+
func isMuted(ctx context.Context, client *Client, checkable *ObjectQueriesResult[HostServiceRuntimeAttributes]) (bool, error) {
28+
if checkable.Attrs.Acknowledgement != AcknowledgementNone || checkable.Attrs.DowntimeDepth != 0 {
29+
return true, nil
30+
}
31+
32+
if checkable.Attrs.IsFlapping && checkable.Attrs.EnableFlapping {
33+
status, err := client.fetchIcingaAppStatus(ctx)
34+
if err != nil {
35+
return false, err
36+
}
37+
38+
return status.App.EnableFlapping, nil
39+
}
40+
41+
return false, nil
2342
}

0 commit comments

Comments
 (0)