Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions alerting/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ dependencies {
// Needed for integ tests
zipArchive group: 'org.opensearch.plugin', name:'opensearch-notifications-core', version: "${opensearch_build}"
zipArchive group: 'org.opensearch.plugin', name:'notifications', version: "${opensearch_build}"
zipArchive group: 'org.opensearch.plugin', name:'opensearch-job-scheduler', version: "${opensearch_build}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What has been moved to use the job scheduler plugin? I thought Alerting has its own job scheduler

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a needed dependency of the SQL/PPL plugin actually. It is explicitly included here solely for integ test purposes. Without this dependency, integ tests fail.

zipArchive group: 'org.opensearch.plugin', name:'opensearch-sql-plugin', version: "${opensearch_build}"

// Needed for security tests
if (securityEnabled) {
Expand All @@ -168,7 +170,10 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}"
implementation "org.jetbrains:annotations:13.0"

// SQL/PPL plugin dependencies are included in alerting-core
api project(":alerting-core")
implementation 'org.json:json:20240303'

implementation "com.github.seancfoley:ipaddress:5.4.1"
implementation project(path: ":alerting-spi", configuration: 'shadow')

Expand Down Expand Up @@ -246,6 +251,28 @@ testClusters.integTest {
}
}))

plugin(provider({
new RegularFile() {
@Override
File getAsFile() {
return configurations.zipArchive.asFileTree.matching {
include '**/opensearch-job-scheduler*'
}.singleFile
}
}
}))

plugin(provider({
new RegularFile() {
@Override
File getAsFile() {
return configurations.zipArchive.asFileTree.matching {
include '**/opensearch-sql-plugin*'
}.singleFile
}
}
}))

if (securityEnabled) {
plugin(provider({
new RegularFile() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.opensearch.alerting.action.GetEmailGroupAction
import org.opensearch.alerting.action.GetRemoteIndexesAction
import org.opensearch.alerting.action.SearchEmailAccountAction
import org.opensearch.alerting.action.SearchEmailGroupAction
import org.opensearch.alerting.actionv2.IndexMonitorV2Action
import org.opensearch.alerting.alerts.AlertIndices
import org.opensearch.alerting.alerts.AlertIndices.Companion.ALL_ALERT_INDEX_PATTERN
import org.opensearch.alerting.comments.CommentsIndices
Expand All @@ -27,6 +28,7 @@ import org.opensearch.alerting.core.resthandler.RestScheduledJobStatsHandler
import org.opensearch.alerting.core.schedule.JobScheduler
import org.opensearch.alerting.core.settings.LegacyOpenDistroScheduledJobSettings
import org.opensearch.alerting.core.settings.ScheduledJobSettings
import org.opensearch.alerting.modelv2.MonitorV2
import org.opensearch.alerting.remote.monitors.RemoteMonitorRegistry
import org.opensearch.alerting.resthandler.RestAcknowledgeAlertAction
import org.opensearch.alerting.resthandler.RestAcknowledgeChainedAlertAction
Expand All @@ -51,6 +53,7 @@ import org.opensearch.alerting.resthandler.RestSearchAlertingCommentAction
import org.opensearch.alerting.resthandler.RestSearchEmailAccountAction
import org.opensearch.alerting.resthandler.RestSearchEmailGroupAction
import org.opensearch.alerting.resthandler.RestSearchMonitorAction
import org.opensearch.alerting.resthandlerv2.RestIndexMonitorV2Action
import org.opensearch.alerting.script.TriggerScript
import org.opensearch.alerting.service.DeleteMonitorService
import org.opensearch.alerting.settings.AlertingSettings
Expand Down Expand Up @@ -83,6 +86,7 @@ import org.opensearch.alerting.transport.TransportSearchAlertingCommentAction
import org.opensearch.alerting.transport.TransportSearchEmailAccountAction
import org.opensearch.alerting.transport.TransportSearchEmailGroupAction
import org.opensearch.alerting.transport.TransportSearchMonitorAction
import org.opensearch.alerting.transportv2.TransportIndexMonitorV2Action
import org.opensearch.alerting.util.DocLevelMonitorQueries
import org.opensearch.alerting.util.destinationmigration.DestinationMigrationCoordinator
import org.opensearch.cluster.metadata.IndexNameExpressionResolver
Expand Down Expand Up @@ -157,6 +161,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
@JvmField val OPEN_SEARCH_DASHBOARDS_USER_AGENT = "OpenSearch-Dashboards"
@JvmField val UI_METADATA_EXCLUDE = arrayOf("monitor.${Monitor.UI_METADATA_FIELD}")
@JvmField val MONITOR_BASE_URI = "/_plugins/_alerting/monitors"
@JvmField val MONITOR_V2_BASE_URI = "/_plugins/_alerting/v2/monitors"
@JvmField val WORKFLOW_BASE_URI = "/_plugins/_alerting/workflows"
@JvmField val REMOTE_BASE_URI = "/_plugins/_alerting/remote"
@JvmField val DESTINATION_BASE_URI = "/_plugins/_alerting/destinations"
Expand All @@ -169,7 +174,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
@JvmField val FINDING_BASE_URI = "/_plugins/_alerting/findings"
@JvmField val COMMENTS_BASE_URI = "/_plugins/_alerting/comments"

@JvmField val ALERTING_JOB_TYPES = listOf("monitor", "workflow")
@JvmField val ALERTING_JOB_TYPES = listOf("monitor", "workflow", "monitor_v2")
}

lateinit var runner: MonitorRunnerService
Expand All @@ -194,6 +199,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
nodesInCluster: Supplier<DiscoveryNodes>
): List<RestHandler> {
return listOf(
// Alerting V1
RestGetMonitorAction(),
RestDeleteMonitorAction(),
RestIndexMonitorAction(),
Expand All @@ -218,11 +224,15 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
RestIndexAlertingCommentAction(),
RestSearchAlertingCommentAction(),
RestDeleteAlertingCommentAction(),

// Alerting V2
RestIndexMonitorV2Action(),
)
}

override fun getActions(): List<ActionPlugin.ActionHandler<out ActionRequest, out ActionResponse>> {
return listOf(
// Alerting V1
ActionPlugin.ActionHandler(ScheduledJobsStatsAction.INSTANCE, ScheduledJobsStatsTransportAction::class.java),
ActionPlugin.ActionHandler(AlertingActions.INDEX_MONITOR_ACTION_TYPE, TransportIndexMonitorAction::class.java),
ActionPlugin.ActionHandler(AlertingActions.GET_MONITOR_ACTION_TYPE, TransportGetMonitorAction::class.java),
Expand All @@ -249,13 +259,17 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
ActionPlugin.ActionHandler(AlertingActions.DELETE_COMMENT_ACTION_TYPE, TransportDeleteAlertingCommentAction::class.java),
ActionPlugin.ActionHandler(ExecuteWorkflowAction.INSTANCE, TransportExecuteWorkflowAction::class.java),
ActionPlugin.ActionHandler(GetRemoteIndexesAction.INSTANCE, TransportGetRemoteIndexesAction::class.java),
ActionPlugin.ActionHandler(DocLevelMonitorFanOutAction.INSTANCE, TransportDocLevelMonitorFanOutAction::class.java)
ActionPlugin.ActionHandler(DocLevelMonitorFanOutAction.INSTANCE, TransportDocLevelMonitorFanOutAction::class.java),

// Alerting V2
ActionPlugin.ActionHandler(IndexMonitorV2Action.INSTANCE, TransportIndexMonitorV2Action::class.java),
)
}

override fun getNamedXContent(): List<NamedXContentRegistry.Entry> {
return listOf(
Monitor.XCONTENT_REGISTRY,
MonitorV2.XCONTENT_REGISTRY,
SearchInput.XCONTENT_REGISTRY,
DocLevelMonitorInput.XCONTENT_REGISTRY,
QueryLevelTrigger.XCONTENT_REGISTRY,
Expand Down Expand Up @@ -431,7 +445,22 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
AlertingSettings.COMMENTS_HISTORY_RETENTION_PERIOD,
AlertingSettings.COMMENTS_MAX_CONTENT_SIZE,
AlertingSettings.MAX_COMMENTS_PER_ALERT,
AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION
AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION,
AlertingSettings.ALERT_V2_HISTORY_ENABLED,
AlertingSettings.ALERT_V2_HISTORY_ROLLOVER_PERIOD,
AlertingSettings.ALERT_V2_HISTORY_INDEX_MAX_AGE,
AlertingSettings.ALERT_V2_HISTORY_MAX_DOCS,
AlertingSettings.ALERT_V2_HISTORY_RETENTION_PERIOD,
AlertingSettings.ALERTING_V2_MAX_MONITORS,
AlertingSettings.ALERTING_V2_MAX_THROTTLE_DURATION,
AlertingSettings.ALERTING_V2_MAX_EXPIRE_DURATION,
AlertingSettings.ALERTING_V2_MAX_LOOK_BACK_WINDOW,
AlertingSettings.ALERTING_V2_MAX_QUERY_LENGTH,
AlertingSettings.ALERTING_V2_QUERY_RESULTS_MAX_DATAROWS,
AlertingSettings.ALERT_V2_QUERY_RESULTS_MAX_SIZE,
AlertingSettings.ALERT_V2_PER_RESULT_TRIGGER_MAX_ALERTS,
AlertingSettings.NOTIFICATION_SUBJECT_SOURCE_MAX_LENGTH,
AlertingSettings.NOTIFICATION_MESSAGE_SOURCE_MAX_LENGTH
)
}

Expand All @@ -449,7 +478,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
return listOf(
SystemIndexDescriptor(ALL_ALERT_INDEX_PATTERN, "Alerting Plugin system index pattern"),
SystemIndexDescriptor(SCHEDULED_JOBS_INDEX, "Alerting Plugin Configuration index"),
SystemIndexDescriptor(ALL_COMMENTS_INDEX_PATTERN, "Alerting Comments system index pattern")
SystemIndexDescriptor(ALL_COMMENTS_INDEX_PATTERN, "Alerting Comments system index pattern"),
)
}

Expand Down
159 changes: 159 additions & 0 deletions alerting/src/main/kotlin/org/opensearch/alerting/AlertingV2Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.alerting

import org.json.JSONArray
import org.json.JSONObject
import org.opensearch.alerting.core.ppl.PPLPluginInterface
import org.opensearch.alerting.modelv2.MonitorV2
import org.opensearch.alerting.opensearchapi.suspendUntil
import org.opensearch.commons.alerting.model.Monitor
import org.opensearch.commons.alerting.model.ScheduledJob
import org.opensearch.commons.alerting.model.Workflow
import org.opensearch.sql.plugin.transport.TransportPPLQueryRequest
import org.opensearch.transport.client.node.NodeClient

object AlertingV2Utils {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move the utils in this function better suited for ppl into a PPL utils class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

// Validates that the given scheduled job is a Monitor
// returns the exception to pass into actionListener.onFailure if not.
fun validateMonitorV1(scheduledJob: ScheduledJob): Exception? {
if (scheduledJob is MonitorV2) {
return IllegalStateException("The ID given corresponds to a V2 Monitor, but a V1 Monitor was expected")
} else if (scheduledJob !is Monitor && scheduledJob !is Workflow) {
return IllegalStateException("The ID given corresponds to a scheduled job of unknown type: ${scheduledJob.javaClass.name}")
}
return null
}

// Validates that the given scheduled job is a MonitorV2
// returns the exception to pass into actionListener.onFailure if not.
fun validateMonitorV2(scheduledJob: ScheduledJob): Exception? {
if (scheduledJob is Monitor || scheduledJob is Workflow) {
return IllegalStateException("The ID given corresponds to a V1 Monitor, but a V2 Monitor was expected")
} else if (scheduledJob !is MonitorV2) {
return IllegalStateException("The ID given corresponds to a scheduled job of unknown type: ${scheduledJob.javaClass.name}")
}
return null
}

// appends user-defined custom trigger condition to PPL query, only for custom condition Triggers
fun appendCustomCondition(query: String, customCondition: String): String {
return "$query | $customCondition"
}

fun appendDataRowsLimit(query: String, maxDataRows: Long): String {
return "$query | head $maxDataRows"
}

// returns PPL query response as parsable JSONObject
suspend fun executePplQuery(query: String, client: NodeClient): JSONObject {
// call PPL plugin to execute query
val transportPplQueryRequest = TransportPPLQueryRequest(
query,
JSONObject(mapOf("query" to query)),
null // null path falls back to a default path internal to SQL/PPL Plugin
)

val transportPplQueryResponse = PPLPluginInterface.suspendUntil {
this.executeQuery(
client,
transportPplQueryRequest,
it
)
}

val queryResponseJson = JSONObject(transportPplQueryResponse.result)

return queryResponseJson
}

// searches a given custom condition eval statement for the name of
// the eval result variable and returns it
fun findEvalResultVar(customCondition: String): String {
// the PPL keyword "eval", followed by a whitespace must be present, otherwise a syntax error from PPL plugin would've
// been thrown when executing the query (without the whitespace, the query would've had something like "evalresult",
// which is invalid PPL
val regex = """\beval\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=""".toRegex()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be a static variable and given our dependency on sql plugin, can we import invalid ppl regex from there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will keep this logic for now but add TODOs to replace these in-house parsers with PPL Plugin functions that likely exist.

val evalResultVar = regex.find(customCondition)?.groupValues?.get(1)
?: throw IllegalArgumentException("Given custom condition is invalid, could not find eval result variable")
return evalResultVar
}

fun findEvalResultVarIdxInSchema(customConditionQueryResponse: JSONObject, evalResultVarName: String): Int {
// find the index eval statement result variable in the PPL query response schema
val schemaList = customConditionQueryResponse.getJSONArray("schema")
var evalResultVarIdx = -1
for (i in 0 until schemaList.length()) {
val schemaObj = schemaList.getJSONObject(i)
val columnName = schemaObj.getString("name")

if (columnName == evalResultVarName) {
evalResultVarIdx = i
break
}
}

// eval statement result variable should always be found
if (evalResultVarIdx == -1) {
throw IllegalStateException(
"expected to find eval statement results variable \"$evalResultVarName\" in results " +
"of PPL query with custom condition, but did not."
)
}

return evalResultVarIdx
}

fun getIndicesFromPplQuery(pplQuery: String): List<String> {
// captures comma-separated concrete indices, index patterns, and index aliases
val indicesRegex = """(?i)source(?:\s*)=(?:\s*)([-\w.*'+]+(?:\*)?(?:\s*,\s*[-\w.*'+]+\*?)*)\s*\|*""".toRegex()

// use find() instead of findAll() because a PPL query only ever has one source statement
// the only capture group specified in the regex captures the comma separated string of indices/index patterns
val indices = indicesRegex.find(pplQuery)?.groupValues?.get(1)?.split(",")?.map { it.trim() }
?: throw IllegalStateException(
"Could not find indices that PPL Monitor query searches even " +
"after validating the query through SQL/PPL plugin"
)

return indices
}

fun capPplQueryResultsSize(pplQueryResults: JSONObject, maxSize: Long): JSONObject {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: use PPL instead of Ppl

/*
the query results JSON object schema:
schema: an array of objects storing the data types of each value of the query result rows, in order
datarows: an array of arrays storing the query results themselves
total: total number of results / data rows
size: same as total, redundant field
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use java docs instead at the top of these functions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do


// estimate byte size with serialized string length
// if query results size are already under the limit, do nothing
// and return the query results as is
val pplQueryResultsSize = pplQueryResults.toString().length
if (pplQueryResultsSize <= maxSize) {
return pplQueryResults
}

// if the query results exceed the limit, we need to replace the query results
// with a message that says the results were too large, but still retain the other
// ppl query response fields like schema, total, and size
val limitExceedMessageQueryResults = JSONObject()

val schema = JSONArray(pplQueryResults.getJSONArray("schema").toList())
val datarows = JSONArray().put(JSONArray(listOf("The PPL Query results were too large and thus excluded")))
val total = pplQueryResults.getInt("total")
val size = pplQueryResults.getInt("size")

limitExceedMessageQueryResults.put("schema", schema)
limitExceedMessageQueryResults.put("datarows", datarows)
limitExceedMessageQueryResults.put("total", total)
limitExceedMessageQueryResults.put("size", size)

return limitExceedMessageQueryResults
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.alerting.actionv2

import org.opensearch.action.ActionType

class IndexMonitorV2Action private constructor() : ActionType<IndexMonitorV2Response>(NAME, ::IndexMonitorV2Response) {
companion object {
val INSTANCE = IndexMonitorV2Action()
const val NAME = "cluster:admin/opensearch/alerting/v2/monitor/write"
}
}
Loading
Loading