Skip to content

Commit c0f99aa

Browse files
committed
Ignore flapping state if flapping detection isn't enabled
1 parent 9864d0e commit c0f99aa

File tree

4 files changed

+88
-10
lines changed

4 files changed

+88
-10
lines changed

internal/icinga2/api_responses.go

+7
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,12 @@ type ObjectCreatedDeleted struct {
352353
EventType string `json:"type"`
353354
}
354355

356+
// IcingaAppStatus 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 IcingaAppStatus struct {
359+
EnableFlapping bool `json:"enable_flapping"`
360+
}
361+
355362
// UnmarshalEventStreamResponse unmarshal a JSON response line from the Icinga 2 API Event Stream.
356363
//
357364
// 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

+51-5
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,47 @@ 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) (*IcingaAppStatus, 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 struct {
172+
App *IcingaAppStatus `json:"app"`
173+
} `json:"icingaapplication"`
174+
} `json:"status"`
175+
}
176+
177+
var results []status
178+
err = json.NewDecoder(response).Decode(&struct {
179+
Results *[]status `json:"results"`
180+
}{&results})
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
app := new(IcingaAppStatus)
186+
if len(results) != 0 {
187+
app = results[0].Status.IcingaApplication.App
188+
}
189+
190+
return app, nil
191+
}
192+
152193
// fetchCheckable fetches the Checkable config state of the given Host/Service name from the Icinga 2 API.
153194
func (client *Client) fetchCheckable(ctx context.Context, host, service string) (*ObjectQueriesResult[HostServiceRuntimeAttributes], error) {
154195
objType, objName := "host", host
@@ -260,8 +301,13 @@ func (client *Client) checkMissedChanges(ctx context.Context, objType string, ca
260301
}
261302

262303
attrs := objQueriesResult.Attrs
304+
checkableIsMuted, err := isMuted(ctx, client, &objQueriesResult)
305+
if err != nil {
306+
return err
307+
}
308+
263309
var fakeEv *event.Event
264-
if attrs.Acknowledgement != AcknowledgementNone {
310+
if checkableIsMuted && attrs.Acknowledgement != AcknowledgementNone {
265311
ackComment, err := client.fetchAcknowledgementComment(ctx, hostName, serviceName, attrs.AcknowledgementLastChange.Time())
266312
if err != nil {
267313
return fmt.Errorf("fetching acknowledgement comment for %q failed, %w", objectName, err)
@@ -275,17 +321,17 @@ func (client *Client) checkMissedChanges(ctx context.Context, objType string, ca
275321
if err != nil {
276322
return fmt.Errorf("failed to construct Event from Acknowledgement response, %w", err)
277323
}
278-
} else if isMuted(&objQueriesResult) {
324+
} else if checkableIsMuted {
279325
fakeEv, err = client.buildCommonEvent(ctx, hostName, serviceName)
280326
if err != nil {
281327
return fmt.Errorf("failed to construct checkable fake mute event: %w", err)
282328
}
283329

284330
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 {
331+
if attrs.DowntimeDepth != 0 {
288332
fakeEv.SetMute(true, "Checkable is in downtime, but we missed the Icinga 2 DowntimeStart event")
333+
} else {
334+
fakeEv.SetMute(true, "Checkable is flapping, but we missed the Icinga 2 FlappingStart event")
289335
}
290336
} else {
291337
// 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.EnableFlapping, nil
39+
}
40+
41+
return false, nil
2342
}

0 commit comments

Comments
 (0)