@@ -13,10 +13,12 @@ import org.opensearch.action.search.SearchRequest
13
13
import org.opensearch.action.search.SearchResponse
14
14
import org.opensearch.action.support.WriteRequest
15
15
import org.opensearch.alerting.model.ActionRunResult
16
+ import org.opensearch.alerting.model.AlertContext
16
17
import org.opensearch.alerting.model.BucketLevelTriggerRunResult
17
18
import org.opensearch.alerting.model.InputRunResults
18
19
import org.opensearch.alerting.model.MonitorRunResult
19
20
import org.opensearch.alerting.opensearchapi.InjectorContextElement
21
+ import org.opensearch.alerting.opensearchapi.convertToMap
20
22
import org.opensearch.alerting.opensearchapi.retry
21
23
import org.opensearch.alerting.opensearchapi.suspendUntil
22
24
import org.opensearch.alerting.opensearchapi.withClosableContext
@@ -25,7 +27,9 @@ import org.opensearch.alerting.util.defaultToPerExecutionAction
25
27
import org.opensearch.alerting.util.getActionExecutionPolicy
26
28
import org.opensearch.alerting.util.getBucketKeysHash
27
29
import org.opensearch.alerting.util.getCombinedTriggerRunResult
30
+ import org.opensearch.alerting.util.printsSampleDocData
28
31
import org.opensearch.alerting.workflow.WorkflowRunContext
32
+ import org.opensearch.client.Client
29
33
import org.opensearch.common.xcontent.LoggingDeprecationHandler
30
34
import org.opensearch.common.xcontent.XContentType
31
35
import org.opensearch.commons.alerting.model.Alert
@@ -220,6 +224,8 @@ object BucketLevelMonitorRunner : MonitorRunner() {
220
224
?.addAll(monitorCtx.alertService!! .convertToCompletedAlerts(keysToAlertsMap))
221
225
}
222
226
227
+ // The alertSampleDocs map structure is Map<TriggerId, Map<BucketKeysHash, List<Alert>>>
228
+ val alertSampleDocs = mutableMapOf<String , Map <String , List <Map <String , Any >>>>()
223
229
for (trigger in monitor.triggers) {
224
230
val alertsToUpdate = mutableSetOf<Alert >()
225
231
val completedAlertsToUpdate = mutableSetOf<Alert >()
@@ -230,6 +236,32 @@ object BucketLevelMonitorRunner : MonitorRunner() {
230
236
? : mutableListOf ()
231
237
// Update nextAlerts so the filtered DEDUPED Alerts are reflected for PER_ALERT Action execution
232
238
nextAlerts[trigger.id]?.set(AlertCategory .DEDUPED , dedupedAlerts)
239
+
240
+ // Only collect sample docs for triggered triggers, and only when at least 1 action prints sample doc data.
241
+ val isTriggered = ! nextAlerts[trigger.id]?.get(AlertCategory .NEW ).isNullOrEmpty()
242
+ if (isTriggered && printsSampleDocData(trigger)) {
243
+ try {
244
+ val searchRequest = monitorCtx.inputService!! .getSearchRequest(
245
+ monitor = monitor.copy(triggers = listOf (trigger)),
246
+ searchInput = monitor.inputs[0 ] as SearchInput ,
247
+ periodStart = periodStart,
248
+ periodEnd = periodEnd,
249
+ prevResult = monitorResult.inputResults,
250
+ matchingDocIdsPerIndex = null ,
251
+ returnSampleDocs = true
252
+ )
253
+ val sampleDocumentsByBucket = getSampleDocs(
254
+ client = monitorCtx.client!! ,
255
+ monitorId = monitor.id,
256
+ triggerId = trigger.id,
257
+ searchRequest = searchRequest
258
+ )
259
+ alertSampleDocs[trigger.id] = sampleDocumentsByBucket
260
+ } catch (e: Exception ) {
261
+ logger.error(" Error retrieving sample documents for trigger {} of monitor {}." , trigger.id, monitor.id, e)
262
+ }
263
+ }
264
+
233
265
val newAlerts = nextAlerts[trigger.id]?.get(AlertCategory .NEW ) ? : mutableListOf ()
234
266
val completedAlerts = nextAlerts[trigger.id]?.get(AlertCategory .COMPLETED ) ? : mutableListOf ()
235
267
@@ -255,8 +287,11 @@ object BucketLevelMonitorRunner : MonitorRunner() {
255
287
for (alertCategory in actionExecutionScope.actionableAlerts) {
256
288
val alertsToExecuteActionsFor = nextAlerts[trigger.id]?.get(alertCategory) ? : mutableListOf ()
257
289
for (alert in alertsToExecuteActionsFor) {
290
+ val alertContext = if (alertCategory != AlertCategory .NEW ) AlertContext (alert = alert)
291
+ else getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs)
292
+
258
293
val actionCtx = getActionContextForAlertCategory(
259
- alertCategory, alert , triggerCtx, monitorOrTriggerError
294
+ alertCategory, alertContext , triggerCtx, monitorOrTriggerError
260
295
)
261
296
// AggregationResultBucket should not be null here
262
297
val alertBucketKeysHash = alert.aggregationResultBucket!! .getBucketKeysHash()
@@ -287,7 +322,9 @@ object BucketLevelMonitorRunner : MonitorRunner() {
287
322
288
323
val actionCtx = triggerCtx.copy(
289
324
dedupedAlerts = dedupedAlerts,
290
- newAlerts = newAlerts,
325
+ newAlerts = newAlerts.map {
326
+ getAlertContext(alert = it, alertSampleDocs = alertSampleDocs)
327
+ },
291
328
completedAlerts = completedAlerts,
292
329
error = monitorResult.error ? : triggerResult.error
293
330
)
@@ -480,17 +517,93 @@ object BucketLevelMonitorRunner : MonitorRunner() {
480
517
481
518
private fun getActionContextForAlertCategory (
482
519
alertCategory : AlertCategory ,
483
- alert : Alert ,
520
+ alertContext : AlertContext ,
484
521
ctx : BucketLevelTriggerExecutionContext ,
485
522
error : Exception ?
486
523
): BucketLevelTriggerExecutionContext {
487
524
return when (alertCategory) {
488
525
AlertCategory .DEDUPED ->
489
- ctx.copy(dedupedAlerts = listOf (alert), newAlerts = emptyList(), completedAlerts = emptyList(), error = error)
526
+ ctx.copy(dedupedAlerts = listOf (alertContext. alert), newAlerts = emptyList(), completedAlerts = emptyList(), error = error)
490
527
AlertCategory .NEW ->
491
- ctx.copy(dedupedAlerts = emptyList(), newAlerts = listOf (alert ), completedAlerts = emptyList(), error = error)
528
+ ctx.copy(dedupedAlerts = emptyList(), newAlerts = listOf (alertContext ), completedAlerts = emptyList(), error = error)
492
529
AlertCategory .COMPLETED ->
493
- ctx.copy(dedupedAlerts = emptyList(), newAlerts = emptyList(), completedAlerts = listOf (alert), error = error)
530
+ ctx.copy(dedupedAlerts = emptyList(), newAlerts = emptyList(), completedAlerts = listOf (alertContext.alert), error = error)
531
+ }
532
+ }
533
+
534
+ private fun getAlertContext (
535
+ alert : Alert ,
536
+ alertSampleDocs : Map <String , Map <String , List <Map <String , Any >>>>
537
+ ): AlertContext {
538
+ val bucketKey = alert.aggregationResultBucket?.getBucketKeysHash()
539
+ val sampleDocs = alertSampleDocs[alert.triggerId]?.get(bucketKey)
540
+ return if (! bucketKey.isNullOrEmpty() && ! sampleDocs.isNullOrEmpty()) {
541
+ AlertContext (alert = alert, sampleDocs = sampleDocs)
542
+ } else {
543
+ logger.error(
544
+ " Failed to retrieve sample documents for alert {} from trigger {} of monitor {} during execution {}." ,
545
+ alert.id,
546
+ alert.triggerId,
547
+ alert.monitorId,
548
+ alert.executionId
549
+ )
550
+ AlertContext (alert = alert, sampleDocs = listOf ())
494
551
}
495
552
}
553
+
554
+ /* *
555
+ * Executes the monitor's query with the addition of 2 top_hits aggregations that are used to return the top 5,
556
+ * and bottom 5 documents for each bucket.
557
+ *
558
+ * @return Map<BucketKeysHash, List<Alert>>
559
+ */
560
+ @Suppress(" UNCHECKED_CAST" )
561
+ private suspend fun getSampleDocs (
562
+ client : Client ,
563
+ monitorId : String ,
564
+ triggerId : String ,
565
+ searchRequest : SearchRequest
566
+ ): Map <String , List <Map <String , Any >>> {
567
+ val sampleDocumentsByBucket = mutableMapOf<String , List <Map <String , Any >>>()
568
+ val searchResponse: SearchResponse = client.suspendUntil { client.search(searchRequest, it) }
569
+ val aggs = searchResponse.convertToMap().getOrDefault(" aggregations" , mapOf<String , Any >()) as Map <String , Any >
570
+ val compositeAgg = aggs.getOrDefault(" composite_agg" , mapOf<String , Any >()) as Map <String , Any >
571
+ val buckets = compositeAgg.getOrDefault(" buckets" , emptyList<Map <String , Any >>()) as List <Map <String , Any >>
572
+
573
+ buckets.forEach { bucket ->
574
+ val bucketKey = getBucketKeysHash((bucket.getOrDefault(" key" , mapOf<String , String >()) as Map <String , String >).values.toList())
575
+ if (bucketKey.isEmpty()) throw IllegalStateException (" Cannot format bucket keys." )
576
+
577
+ val unwrappedTopHits = (bucket.getOrDefault(" top_hits" , mapOf<String , Any >()) as Map <String , Any >)
578
+ .getOrDefault(" hits" , mapOf<String , Any >()) as Map <String , Any >
579
+ val topHits = unwrappedTopHits.getOrDefault(" hits" , listOf<Map <String , Any >>()) as List <Map <String , Any >>
580
+
581
+ val unwrappedLowHits = (bucket.getOrDefault(" low_hits" , mapOf<String , Any >()) as Map <String , Any >)
582
+ .getOrDefault(" hits" , mapOf<String , Any >()) as Map <String , Any >
583
+ val lowHits = unwrappedLowHits.getOrDefault(" hits" , listOf<Map <String , Any >>()) as List <Map <String , Any >>
584
+
585
+ // Reversing the order of lowHits so allHits will be in descending order.
586
+ val allHits = topHits + lowHits.reversed()
587
+
588
+ if (allHits.isEmpty()) {
589
+ // We expect sample documents to be available for each bucket.
590
+ logger.error(" Sample documents not found for trigger {} of monitor {}." , triggerId, monitorId)
591
+ }
592
+
593
+ // Removing duplicate hits. The top_hits, and low_hits results return a max of 5 docs each.
594
+ // The same document could be present in both hit lists if there are fewer than 10 documents in the bucket of data.
595
+ val uniqueHitIds = mutableSetOf<String >()
596
+ val dedupedHits = mutableListOf<Map <String , Any >>()
597
+ allHits.forEach { hit ->
598
+ val hitId = hit[" _id" ] as String
599
+ if (! uniqueHitIds.contains(hitId)) {
600
+ uniqueHitIds.add(hitId)
601
+ dedupedHits.add(hit)
602
+ }
603
+ }
604
+ sampleDocumentsByBucket[bucketKey] = dedupedHits
605
+ }
606
+
607
+ return sampleDocumentsByBucket
608
+ }
496
609
}
0 commit comments