7
7
"context"
8
8
"crypto/rand"
9
9
"encoding/json"
10
+ "errors"
10
11
"fmt"
11
12
"github.com/icinga/icinga-notifications/internal/event"
12
13
"go.uber.org/zap"
@@ -212,17 +213,28 @@ func (client *Client) fetchCheckable(ctx context.Context, host, service string)
212
213
return & objQueriesResults [0 ], nil
213
214
}
214
215
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
+
215
222
// fetchAcknowledgementComment fetches an Acknowledgement Comment for a Host (empty service) or for a Service at a Host.
216
223
//
217
224
// Unfortunately, there is no direct link between ACK'ed Host or Service objects and their acknowledgement Comment. The
218
225
// 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
220
227
// 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.
221
231
func (client * Client ) fetchAcknowledgementComment (ctx context.Context , host , service string , ackTime time.Time ) (* Comment , error ) {
222
232
// comment.entry_type = 4 is an Acknowledgement comment; Comment.EntryType
233
+ objectName := host
223
234
filterExpr := "comment.entry_type == 4 && comment.host_name == comment_host_name"
224
235
filterVars := map [string ]string {"comment_host_name" : host }
225
236
if service != "" {
237
+ objectName += "!" + service
226
238
filterExpr += " && comment.service_name == comment_service_name"
227
239
filterVars ["comment_service_name" ] = service
228
240
}
@@ -237,7 +249,7 @@ func (client *Client) fetchAcknowledgementComment(ctx context.Context, host, ser
237
249
}
238
250
239
251
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 )
241
253
}
242
254
243
255
slices .SortFunc (objQueriesResults , func (a , b ObjectQueriesResult [Comment ]) int {
@@ -246,7 +258,7 @@ func (client *Client) fetchAcknowledgementComment(ctx context.Context, host, ser
246
258
return cmp .Compare (distA , distB )
247
259
})
248
260
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 )
250
262
}
251
263
252
264
return & objQueriesResults [0 ].Attrs , nil
@@ -306,17 +318,38 @@ func (client *Client) checkMissedChanges(ctx context.Context, objType string, ca
306
318
var fakeEv * event.Event
307
319
if checkableIsMuted && attrs .Acknowledgement != AcknowledgementNone {
308
320
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
+ }
312
339
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
+ }
320
353
}
321
354
} else if checkableIsMuted {
322
355
fakeEv , err = client .buildCommonEvent (ctx , hostName , serviceName )
0 commit comments