Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.opensearch.cluster.service.ClusterService
import org.opensearch.common.inject.Inject
import org.opensearch.common.settings.Settings
import org.opensearch.commons.alerting.model.Monitor
import org.opensearch.commons.alerting.model.ScheduledJob
import org.opensearch.commons.authuser.User
import org.opensearch.index.query.BoolQueryBuilder
import org.opensearch.index.query.ExistsQueryBuilder
Expand Down Expand Up @@ -52,9 +53,18 @@ class TransportSearchMonitorAction @Inject constructor(

override fun doExecute(task: Task, searchMonitorRequest: SearchMonitorRequest, actionListener: ActionListener<SearchResponse>) {
val searchSourceBuilder = searchMonitorRequest.searchRequest.source()
.seqNoAndPrimaryTerm(true)
.version(true)
val queryBuilder = if (searchSourceBuilder.query() == null) BoolQueryBuilder()
else QueryBuilders.boolQuery().must(searchSourceBuilder.query())
queryBuilder.filter(QueryBuilders.existsQuery(Monitor.MONITOR_TYPE))

// The SearchMonitor API supports one 'index' parameter of either the SCHEDULED_JOBS_INDEX or ALL_ALERT_INDEX_PATTERN.
// When querying the ALL_ALERT_INDEX_PATTERN, we don't want to check whether the MONITOR_TYPE field exists
// because we're querying alert indexes.
if (searchMonitorRequest.searchRequest.indices().contains(ScheduledJob.SCHEDULED_JOBS_INDEX)) {
queryBuilder.filter(QueryBuilders.existsQuery(Monitor.MONITOR_TYPE))
}

searchSourceBuilder.query(queryBuilder)
.seqNoAndPrimaryTerm(true)
.version(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package org.opensearch.alerting.resthandler

import org.apache.http.HttpHeaders
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import org.apache.http.message.BasicHeader
import org.apache.http.nio.entity.NStringEntity
import org.opensearch.alerting.ALERTING_BASE_URI
Expand Down Expand Up @@ -54,7 +55,9 @@ import org.opensearch.core.xcontent.XContentBuilder
import org.opensearch.index.query.QueryBuilders
import org.opensearch.rest.RestStatus
import org.opensearch.script.Script
import org.opensearch.search.aggregations.AggregationBuilders
import org.opensearch.search.builder.SearchSourceBuilder
import org.opensearch.search.sort.SortOrder
import org.opensearch.test.OpenSearchTestCase
import org.opensearch.test.junit.annotations.TestLogging
import org.opensearch.test.rest.OpenSearchRestTestCase
Expand Down Expand Up @@ -1264,6 +1267,118 @@ class MonitorRestApiIT : AlertingRestTestCase() {
}
}

/**
* This use case is needed by the frontend plugin for displaying alert counts on the Monitors list page.
* https://github.com/opensearch-project/alerting-dashboards-plugin/blob/main/server/services/MonitorService.js#L235
*/
fun `test get acknowledged, active, error, and ignored alerts counts`() {
putAlertMappings()
val monitorAlertCounts = hashMapOf<String, HashMap<String, Int>>()
val numMonitors = randomIntBetween(1, 10)
repeat(numMonitors) {
val monitor = createRandomMonitor(refresh = true)

val numAcknowledgedAlerts = randomIntBetween(1, 10)
val numActiveAlerts = randomIntBetween(1, 10)
var numCompletedAlerts = randomIntBetween(1, 10)
val numErrorAlerts = randomIntBetween(1, 10)
val numIgnoredAlerts = randomIntBetween(1, numCompletedAlerts)
numCompletedAlerts -= numIgnoredAlerts

val alertCounts = hashMapOf(
Alert.State.ACKNOWLEDGED.name to numAcknowledgedAlerts,
Alert.State.ACTIVE.name to numActiveAlerts,
Alert.State.COMPLETED.name to numCompletedAlerts,
Alert.State.ERROR.name to numErrorAlerts,
"IGNORED" to numIgnoredAlerts
)
monitorAlertCounts[monitor.id] = alertCounts

repeat(numAcknowledgedAlerts) {
createAlert(randomAlert(monitor).copy(acknowledgedTime = Instant.now(), state = Alert.State.ACKNOWLEDGED))
}
repeat(numActiveAlerts) {
createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE))
}
repeat(numCompletedAlerts) {
createAlert(randomAlert(monitor).copy(acknowledgedTime = Instant.now(), state = Alert.State.COMPLETED))
}
repeat(numErrorAlerts) {
createAlert(randomAlert(monitor).copy(state = Alert.State.ERROR))
}
repeat(numIgnoredAlerts) {
createAlert(randomAlert(monitor).copy(acknowledgedTime = null, state = Alert.State.COMPLETED))
}
}

val sourceBuilder = SearchSourceBuilder()
.size(0)
.query(QueryBuilders.termsQuery("monitor_id", monitorAlertCounts.keys))
.aggregation(
AggregationBuilders
.terms("uniq_monitor_ids").field("monitor_id")
.subAggregation(AggregationBuilders.filter("active", QueryBuilders.termQuery("state", "ACTIVE")))
.subAggregation(AggregationBuilders.filter("acknowledged", QueryBuilders.termQuery("state", "ACKNOWLEDGED")))
.subAggregation(AggregationBuilders.filter("errors", QueryBuilders.termQuery("state", "ERROR")))
.subAggregation(
AggregationBuilders.filter(
"ignored",
QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery("state", "COMPLETED"))
.mustNot(QueryBuilders.existsQuery("acknowledged_time"))
)
)
.subAggregation(AggregationBuilders.max("last_notification_time").field("last_notification_time"))
.subAggregation(
AggregationBuilders.topHits("latest_alert")
.size(1)
.sort("start_time", SortOrder.DESC)
.fetchSource(arrayOf("last_notification_time", "trigger_name"), null)
)
)

val searchResponse = client().makeRequest(
"GET",
"$ALERTING_BASE_URI/_search",
hashMapOf("index" to AlertIndices.ALL_ALERT_INDEX_PATTERN),
StringEntity(sourceBuilder.toString(), ContentType.APPLICATION_JSON)
)
val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content).map()
val aggregations = (xcp["aggregations"]!! as Map<String, Map<String, Any>>)
val uniqMonitorIds = aggregations["uniq_monitor_ids"]!!
val buckets = uniqMonitorIds["buckets"]!! as ArrayList<Map<String, Any>>

assertEquals("Incorrect number of monitors returned", monitorAlertCounts.keys.size, buckets.size)
buckets.forEach { bucket ->
val id = bucket["key"]!!
val monitorCounts = monitorAlertCounts[id]!!

val acknowledged = (bucket["acknowledged"]!! as Map<String, Int>)["doc_count"]!!
assertEquals(
"Incorrect ${Alert.State.ACKNOWLEDGED} count returned for monitor $id",
monitorCounts[Alert.State.ACKNOWLEDGED.name], acknowledged
)

val active = (bucket["active"]!! as Map<String, Int>)["doc_count"]!!
assertEquals(
"Incorrect ${Alert.State.ACTIVE} count returned for monitor $id",
monitorCounts[Alert.State.ACTIVE.name], active
)

val errors = (bucket["errors"]!! as Map<String, Int>)["doc_count"]!!
assertEquals(
"Incorrect ${Alert.State.ERROR} count returned for monitor $id",
monitorCounts[Alert.State.ERROR.name], errors
)

val ignored = (bucket["ignored"]!! as Map<String, Int>)["doc_count"]!!
assertEquals(
"Incorrect IGNORED count returned for monitor $id",
monitorCounts["IGNORED"], ignored
)
}
}

private fun validateAlertingStatsNodeResponse(nodesResponse: Map<String, Int>) {
assertEquals("Incorrect number of nodes", numberOfNodes, nodesResponse["total"])
assertEquals("Failed nodes found during monitor stats call", 0, nodesResponse["failed"])
Expand Down