Skip to content

Commit

Permalink
Fixed search monitor API to return alert counts. (#978)
Browse files Browse the repository at this point in the history
* Fixed a bug in the search monitor API when which prevent alert counts from being returned.

Signed-off-by: AWSHurneyt <[email protected]>

* Implemented integration test for frontend use case.

Signed-off-by: AWSHurneyt <[email protected]>

* Fixed ktlint errors.

Signed-off-by: AWSHurneyt <[email protected]>

* Fixed ktlint errors.

Signed-off-by: AWSHurneyt <[email protected]>

* Removed redundant code.

Signed-off-by: AWSHurneyt <[email protected]>

* Fixed ktlint errors.

Signed-off-by: AWSHurneyt <[email protected]>

* Added back check for null query.

Signed-off-by: AWSHurneyt <[email protected]>

---------

Signed-off-by: AWSHurneyt <[email protected]>
  • Loading branch information
AWSHurneyt authored Jul 10, 2023
1 parent 6da86fe commit 641d17b
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ import org.opensearch.common.settings.Settings
import org.opensearch.common.xcontent.LoggingDeprecationHandler
import org.opensearch.common.xcontent.XContentFactory.jsonBuilder
import org.opensearch.common.xcontent.XContentType
import org.opensearch.commons.alerting.model.Monitor
import org.opensearch.commons.alerting.model.ScheduledJob
import org.opensearch.commons.alerting.model.ScheduledJob.Companion.SCHEDULED_JOBS_INDEX
import org.opensearch.core.xcontent.ToXContent.EMPTY_PARAMS
import org.opensearch.index.query.QueryBuilders
import org.opensearch.rest.BaseRestHandler
import org.opensearch.rest.BaseRestHandler.RestChannelConsumer
import org.opensearch.rest.BytesRestResponse
Expand Down Expand Up @@ -97,14 +95,6 @@ class RestSearchMonitorAction(
searchSourceBuilder.parseXContent(request.contentOrSourceParamParser())
searchSourceBuilder.fetchSource(context(request))

val queryBuilder = QueryBuilders.boolQuery().must(searchSourceBuilder.query())
if (index == SCHEDULED_JOBS_INDEX) {
queryBuilder.filter(QueryBuilders.existsQuery(Monitor.MONITOR_TYPE))
}

searchSourceBuilder.query(queryBuilder)
.seqNoAndPrimaryTerm(true)
.version(true)
val searchRequest = SearchRequest()
.source(searchSourceBuilder)
.indices(index)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 @@ -50,9 +51,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 @@ -54,7 +54,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 @@ -1263,6 +1265,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

0 comments on commit 641d17b

Please sign in to comment.