Skip to content

Commit b3a137e

Browse files
committed
icinga2: Common Muted Event for Acknowledgements w/o Comments during Catch-Up
When creating an acknowledgement in Icinga Web, a comment is also added. It is possible, however, to later delete the comment while keeping the object acknowledged. Deleting acknowledgement comments was not an expected behavior, requiring the presence of an associated comment for each acknowledgement. Otherwise, the catch-up-phase would not succeed. Unfortunately, there is no way to later add an author to an acknowledgement unless a matching comment exists. Thus, the catch-up-phase will now generalize this acknowledgement into a common muted event, missing the details available in the comment. Closes #245.
1 parent d7dab21 commit b3a137e

File tree

1 file changed

+46
-13
lines changed

1 file changed

+46
-13
lines changed

internal/icinga2/client_api.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"crypto/rand"
99
"encoding/json"
10+
"errors"
1011
"fmt"
1112
"github.com/icinga/icinga-notifications/internal/event"
1213
"go.uber.org/zap"
@@ -212,17 +213,28 @@ func (client *Client) fetchCheckable(ctx context.Context, host, service string)
212213
return &objQueriesResults[0], nil
213214
}
214215

216+
// errMissingAcknowledgementComment is an error indicating that no Comment for an Acknowledgement exists.
217+
//
218+
// This error should only be wrapped and returned from the fetchAcknowledgementComment method and only if no Comment was
219+
// found. For other errors, like network errors, this error must not be used.
220+
var errMissingAcknowledgementComment = errors.New("found no acknowledgement comment")
221+
215222
// fetchAcknowledgementComment fetches an Acknowledgement Comment for a Host (empty service) or for a Service at a Host.
216223
//
217224
// Unfortunately, there is no direct link between ACK'ed Host or Service objects and their acknowledgement Comment. The
218225
// closest we can do, is query for Comments with the Acknowledgement Service Type and the host/service name. In addition,
219-
// the Host's resp. Service's AcknowledgementLastChange field has NOT the same timestamp as the Comment; there is a
226+
// the Host's or Service's AcknowledgementLastChange field has NOT the same timestamp as the Comment; there is a
220227
// difference of some milliseconds. As there might be even multiple ACK comments, we have to find the closest one.
228+
//
229+
// Please note that not every Acknowledgement has a Comment. It is possible to delete the Comment, while still having an
230+
// active Acknowledgement. Thus, if no Comment was found, a wrapped errMissingAcknowledgementComment is returned.
221231
func (client *Client) fetchAcknowledgementComment(ctx context.Context, host, service string, ackTime time.Time) (*Comment, error) {
222232
// comment.entry_type = 4 is an Acknowledgement comment; Comment.EntryType
233+
objectName := host
223234
filterExpr := "comment.entry_type == 4 && comment.host_name == comment_host_name"
224235
filterVars := map[string]string{"comment_host_name": host}
225236
if service != "" {
237+
objectName += "!" + service
226238
filterExpr += " && comment.service_name == comment_service_name"
227239
filterVars["comment_service_name"] = service
228240
}
@@ -237,7 +249,7 @@ func (client *Client) fetchAcknowledgementComment(ctx context.Context, host, ser
237249
}
238250

239251
if len(objQueriesResults) == 0 {
240-
return nil, fmt.Errorf("found no ACK Comments for %q with %v", filterExpr, filterVars)
252+
return nil, fmt.Errorf("%w for %q", errMissingAcknowledgementComment, objectName)
241253
}
242254

243255
slices.SortFunc(objQueriesResults, func(a, b ObjectQueriesResult[Comment]) int {
@@ -246,7 +258,7 @@ func (client *Client) fetchAcknowledgementComment(ctx context.Context, host, ser
246258
return cmp.Compare(distA, distB)
247259
})
248260
if objQueriesResults[0].Attrs.EntryTime.Time().Sub(ackTime).Abs() > time.Second {
249-
return nil, fmt.Errorf("found no ACK Comment for %q with %v close to %v", filterExpr, filterVars, ackTime)
261+
return nil, fmt.Errorf("%w for %q near %v", errMissingAcknowledgementComment, objectName, ackTime)
250262
}
251263

252264
return &objQueriesResults[0].Attrs, nil
@@ -306,17 +318,38 @@ func (client *Client) checkMissedChanges(ctx context.Context, objType string, ca
306318
var fakeEv *event.Event
307319
if checkableIsMuted && attrs.Acknowledgement != AcknowledgementNone {
308320
ackComment, err := client.fetchAcknowledgementComment(ctx, hostName, serviceName, attrs.AcknowledgementLastChange.Time())
309-
if err != nil {
310-
return fmt.Errorf("fetching acknowledgement comment for %q failed, %w", objectName, err)
311-
}
321+
if errors.Is(err, errMissingAcknowledgementComment) {
322+
// Unfortunately, there is no Acknowledgement object in Icinga 2, but only related runtime attributes
323+
// attached to Host or Service objects. Those attributes contain no authorship. The only way to link an
324+
// acknowledgement to a contact, when being fetched through the Config Objects API, is to find a
325+
// matching Comment object, which contains an author field.
326+
//
327+
// This is not the case for the Event Stream API, where AcknowledgementSet has an author field.
328+
//
329+
// However, when no author is present, the Acknowledgement Event cannot be processed. Eventually, the
330+
// Incident.processAcknowledgementEvent method will fail hard.
331+
332+
client.Logger.Infow("Cannot find the comment for an acknowledgement, creating a generic muted event",
333+
zap.String("object", objectName), zap.Error(err))
334+
335+
fakeEv, err = client.buildCommonEvent(ctx, hostName, serviceName)
336+
if err != nil {
337+
return fmt.Errorf("failed to construct checkable fake unmute event: %w", err)
338+
}
312339

313-
ack := &Acknowledgement{Host: hostName, Service: serviceName, Author: ackComment.Author, Comment: ackComment.Text}
314-
// We do not need to fake ACK set events as they are handled correctly by an incident and any
315-
// redundant/successive ACK set events are discarded accordingly.
316-
ack.EventType = typeAcknowledgementSet
317-
fakeEv, err = client.buildAcknowledgementEvent(ctx, ack)
318-
if err != nil {
319-
return fmt.Errorf("failed to construct Event from Acknowledgement response, %w", err)
340+
fakeEv.Type = event.TypeMute
341+
fakeEv.SetMute(false, "Acknowledgement event without corresponding comment")
342+
} else if err != nil {
343+
return fmt.Errorf("fetching acknowledgement comment for %q failed, %w", objectName, err)
344+
} else {
345+
ack := &Acknowledgement{Host: hostName, Service: serviceName, Author: ackComment.Author, Comment: ackComment.Text}
346+
// We do not need to fake ACK set events as they are handled correctly by an incident and any
347+
// redundant/successive ACK set events are discarded accordingly.
348+
ack.EventType = typeAcknowledgementSet
349+
fakeEv, err = client.buildAcknowledgementEvent(ctx, ack)
350+
if err != nil {
351+
return fmt.Errorf("failed to construct Event from Acknowledgement response, %w", err)
352+
}
320353
}
321354
} else if checkableIsMuted {
322355
fakeEv, err = client.buildCommonEvent(ctx, hostName, serviceName)

0 commit comments

Comments
 (0)