From 2ebfb97cb65401ec301b77251552405b2faef281 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 21 Oct 2025 15:59:54 -0700 Subject: [PATCH 01/13] PPL Alerting Models Signed-off-by: Dennis Toepker --- .../opensearch/alerting/modelv2/AlertV2.kt | 265 ++++++++++++ .../opensearch/alerting/modelv2/MonitorV2.kt | 145 +++++++ .../alerting/modelv2/MonitorV2RunResult.kt | 43 ++ .../opensearch/alerting/modelv2/PPLMonitor.kt | 386 +++++++++++++++++ .../alerting/modelv2/PPLMonitorRunResult.kt | 54 +++ .../opensearch/alerting/modelv2/PPLTrigger.kt | 397 ++++++++++++++++++ .../alerting/modelv2/PPLTriggerRunResult.kt | 56 +++ .../opensearch/alerting/modelv2/TriggerV2.kt | 64 +++ .../alerting/modelv2/TriggerV2RunResult.kt | 22 + .../org/opensearch/alerting/TestHelpers.kt | 302 ++++++++++++- .../alerting/modelv2/AlertV2Tests.kt | 65 +++ .../alerting/modelv2/MonitorV2Tests.kt | 161 +++++++ .../alerting/modelv2/TriggerV2Tests.kt | 243 +++++++++++ .../alerting/core/util/XContentExtensions.kt | 13 + 14 files changed, 2215 insertions(+), 1 deletion(-) create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2RunResult.kt create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt create mode 100644 core/src/main/kotlin/org/opensearch/alerting/core/util/XContentExtensions.kt diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt new file mode 100644 index 000000000..b42f2c2b2 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt @@ -0,0 +1,265 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.core.util.nonOptionalTimeField +import org.opensearch.alerting.modelv2.TriggerV2.Severity +import org.opensearch.common.lucene.uid.Versions +import org.opensearch.commons.alerting.util.instant +import org.opensearch.commons.alerting.util.optionalUserField +import org.opensearch.commons.authuser.User +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.common.io.stream.Writeable +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.core.xcontent.XContentBuilder +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken +import java.io.IOException +import java.time.Instant + +/** + * Alert generated by Alerting V2 + * An alert is created when a Trigger's trigger conditions are met. + * + * @property id Alert ID. Defaults to [NO_ID]. + * @property version Version number of the Alert. Defaults to [NO_VERSION]. + * @property schemaVersion Version of the alerting-alerts index schema when this Alert was indexed. Defaults to [NO_SCHEMA_VERSION]. + * @property monitorId ID of the Monitor that generated this Alert. + * @property monitorName Name of the Monitor that generated this Alert. + * @property monitorVersion Version of the Monitor at the time it generated this Alert. + * @property triggerId ID of the Trigger in the Monitor that generated this alert. + * @property triggerName Name of the trigger in the Monitor that generated this alert. + * @property queryResults Results from the Monitor's query that caused the Trigger to fire. + * @property triggeredTime Timestamp for when the Alert was generated. + * @property expirationTime Timestamp for when the Alert should be expired. + * @property errorMessage Optional error message if there were issues during Trigger execution. + * Null indicates no errors occurred. + * @property severity Severity level of the alert (e.g., "HIGH", "MEDIUM", "LOW"). + * @property executionId Optional ID for the Monitor execution that generated this Alert. + * + * @see MonitorV2 For the monitor that generates alerts + * @see TriggerV2 For the trigger conditions that create alerts + * + * Lifecycle: + * 1. AlertV2 is generated when a TriggerV2's condition is met. The TriggerV2 fires and forgets the AlertV2. + * 2. AlertV2 is stored in the alerts index. AlertV2s are stateless. (e.g. they are never ACTIVE or COMPLETED) + * 3. AlertV2 is soft deleted at [expirationTime], and archived in an alert history index + * 4. Based on the alert v2 history retention period, the AlertV2 is permanently deleted + */ +data class AlertV2( + val id: String = NO_ID, + val version: Long = NO_VERSION, + val schemaVersion: Int = NO_SCHEMA_VERSION, + val monitorId: String, + val monitorName: String, + val monitorVersion: Long, + val monitorUser: User?, + val triggerId: String, + val triggerName: String, + val query: String, + val queryResults: Map, + val triggeredTime: Instant, + val expirationTime: Instant, + val errorMessage: String? = null, + val severity: Severity, + val executionId: String? = null +) : Writeable, ToXContent { + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + id = sin.readString(), + version = sin.readLong(), + schemaVersion = sin.readInt(), + monitorId = sin.readString(), + monitorName = sin.readString(), + monitorVersion = sin.readLong(), + monitorUser = if (sin.readBoolean()) { + User(sin) + } else { + null + }, + triggerId = sin.readString(), + triggerName = sin.readString(), + query = sin.readString(), + queryResults = sin.readMap(), + triggeredTime = sin.readInstant(), + expirationTime = sin.readInstant(), + errorMessage = sin.readOptionalString(), + severity = sin.readEnum(Severity::class.java), + executionId = sin.readOptionalString() + ) + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(id) + out.writeLong(version) + out.writeInt(schemaVersion) + out.writeString(monitorId) + out.writeString(monitorName) + out.writeLong(monitorVersion) + out.writeBoolean(monitorUser != null) + monitorUser?.writeTo(out) + out.writeString(triggerId) + out.writeString(triggerName) + out.writeString(query) + out.writeMap(queryResults) + out.writeInstant(triggeredTime) + out.writeInstant(expirationTime) + out.writeOptionalString(errorMessage) + out.writeEnum(severity) + out.writeOptionalString(executionId) + } + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + return createXContentBuilder(builder, false) + } + + fun toXContentWithUser(builder: XContentBuilder): XContentBuilder { + return createXContentBuilder(builder, true) + } + + private fun createXContentBuilder(builder: XContentBuilder, withUser: Boolean): XContentBuilder { + builder.startObject() + .field(ALERT_V2_ID_FIELD, id) + .field(ALERT_V2_VERSION_FIELD, version) + .field(MONITOR_V2_ID_FIELD, monitorId) + .field(SCHEMA_VERSION_FIELD, schemaVersion) + .field(MONITOR_V2_VERSION_FIELD, monitorVersion) + .field(MONITOR_V2_NAME_FIELD, monitorName) + .field(EXECUTION_ID_FIELD, executionId) + .field(TRIGGER_V2_ID_FIELD, triggerId) + .field(TRIGGER_V2_NAME_FIELD, triggerName) + .field(QUERY_FIELD, query) + .field(QUERY_RESULTS_FIELD, queryResults) + .field(ERROR_MESSAGE_FIELD, errorMessage) + .field(SEVERITY_FIELD, severity.value) + .nonOptionalTimeField(TRIGGERED_TIME_FIELD, triggeredTime) + .nonOptionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) + + if (withUser) { + builder.optionalUserField(MONITOR_V2_USER_FIELD, monitorUser) + } + + builder.endObject() + + return builder + } + + fun asTemplateArg(): Map { + return mapOf( + ALERT_V2_ID_FIELD to id, + ALERT_V2_VERSION_FIELD to version, + ERROR_MESSAGE_FIELD to errorMessage, + EXECUTION_ID_FIELD to executionId, + EXPIRATION_TIME_FIELD to expirationTime.toEpochMilli(), + SEVERITY_FIELD to severity.value + ) + } + + companion object { + const val ALERT_V2_ID_FIELD = "id" + const val ALERT_V2_VERSION_FIELD = "version" + const val MONITOR_V2_ID_FIELD = "monitor_v2_id" + const val MONITOR_V2_VERSION_FIELD = "monitor_v2_version" + const val MONITOR_V2_NAME_FIELD = "monitor_v2_name" + const val MONITOR_V2_USER_FIELD = "monitor_v2_user" + const val TRIGGER_V2_ID_FIELD = "trigger_v2_id" + const val TRIGGER_V2_NAME_FIELD = "trigger_v2_name" + const val TRIGGERED_TIME_FIELD = "triggered_time" + const val EXPIRATION_TIME_FIELD = "expiration_time" + const val QUERY_FIELD = "query" + const val QUERY_RESULTS_FIELD = "query_results" + const val ERROR_MESSAGE_FIELD = "error_message" + const val EXECUTION_ID_FIELD = "execution_id" + const val SEVERITY_FIELD = "severity" + const val SCHEMA_VERSION_FIELD = "schema_version" + + const val NO_ID = "" + const val NO_VERSION = Versions.NOT_FOUND + const val NO_SCHEMA_VERSION = 0 + + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): AlertV2 { + var schemaVersion = NO_SCHEMA_VERSION + lateinit var monitorId: String + lateinit var monitorName: String + var monitorVersion: Long = Versions.NOT_FOUND + var monitorUser: User? = null + lateinit var triggerId: String + lateinit var triggerName: String + lateinit var query: String + var queryResults: Map = mapOf() + lateinit var severity: Severity + var triggeredTime: Instant? = null + var expirationTime: Instant? = null + var errorMessage: String? = null + var executionId: String? = null + + ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = xcp.currentName() + xcp.nextToken() + + when (fieldName) { + MONITOR_V2_ID_FIELD -> monitorId = xcp.text() + SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() + MONITOR_V2_NAME_FIELD -> monitorName = xcp.text() + MONITOR_V2_VERSION_FIELD -> monitorVersion = xcp.longValue() + MONITOR_V2_USER_FIELD -> + monitorUser = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + User.parse(xcp) + } + TRIGGER_V2_ID_FIELD -> triggerId = xcp.text() + TRIGGER_V2_NAME_FIELD -> triggerName = xcp.text() + QUERY_FIELD -> query = xcp.text() + QUERY_RESULTS_FIELD -> queryResults = xcp.map() + TRIGGERED_TIME_FIELD -> triggeredTime = xcp.instant() + EXPIRATION_TIME_FIELD -> expirationTime = xcp.instant() + ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull() + EXECUTION_ID_FIELD -> executionId = xcp.textOrNull() + TriggerV2.SEVERITY_FIELD -> { + val input = xcp.text() + val enumMatchResult = Severity.enumFromString(input) + ?: throw IllegalArgumentException( + "Invalid value for ${TriggerV2.SEVERITY_FIELD}: $input. " + + "Supported values are ${Severity.entries.map { it.value }}" + ) + severity = enumMatchResult + } + } + } + + return AlertV2( + id = id, + version = version, + schemaVersion = schemaVersion, + monitorId = requireNotNull(monitorId), + monitorName = requireNotNull(monitorName), + monitorVersion = monitorVersion, + monitorUser = monitorUser, + triggerId = requireNotNull(triggerId), + triggerName = requireNotNull(triggerName), + query = requireNotNull(query), + queryResults = requireNotNull(queryResults), + triggeredTime = requireNotNull(triggeredTime), + expirationTime = requireNotNull(expirationTime), + errorMessage = errorMessage, + severity = severity, + executionId = executionId + ) + } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): AlertV2 { + return AlertV2(sin) + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt new file mode 100644 index 000000000..2fff45781 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -0,0 +1,145 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.modelv2.PPLMonitor.Companion.PPL_MONITOR_TYPE +import org.opensearch.common.CheckedFunction +import org.opensearch.commons.alerting.model.Schedule +import org.opensearch.commons.alerting.model.ScheduledJob +import org.opensearch.commons.authuser.User +import org.opensearch.core.ParseField +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.core.xcontent.XContentBuilder +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import java.io.IOException +import java.time.Instant + +interface MonitorV2 : ScheduledJob { + override val id: String + override val version: Long + override val name: String + override val enabled: Boolean + override val schedule: Schedule + override val lastUpdateTime: Instant // required for scheduled job maintenance + override val enabledTime: Instant? // required for scheduled job maintenance + val user: User? + val triggers: List + val schemaVersion: Int // for updating monitors + val lookBackWindow: Long? // how far back to look when querying data during monitor execution + val timestampField: String? // field that will be used to inject lookback window time filter + + fun asTemplateArg(): Map + + fun toXContentWithUser(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder + + fun makeCopy( + id: String = this.id, + version: Long = this.version, + name: String = this.name, + enabled: Boolean = this.enabled, + schedule: Schedule = this.schedule, + lastUpdateTime: Instant = this.lastUpdateTime, + enabledTime: Instant? = this.enabledTime, + user: User? = this.user, + // no support for overriding triggers in copy + schemaVersion: Int = this.schemaVersion, + lookBackWindow: Long? = this.lookBackWindow, + timestampField: String? = this.timestampField + ): MonitorV2 + + enum class MonitorV2Type(val value: String) { + PPL_MONITOR(PPL_MONITOR_TYPE); + + override fun toString(): String { + return value + } + + companion object { + fun enumFromString(value: String): MonitorV2Type? { + return MonitorV2Type.entries.find { it.value == value } + } + } + } + + companion object { + // scheduled job field names + const val MONITOR_V2_TYPE = "monitor_v2" // scheduled job type is MonitorV2 + + // field names + const val NAME_FIELD = "name" + const val ENABLED_FIELD = "enabled" + const val SCHEDULE_FIELD = "schedule" + const val LAST_UPDATE_TIME_FIELD = "last_update_time" + const val ENABLED_TIME_FIELD = "enabled_time" + const val USER_FIELD = "user" + const val TRIGGERS_FIELD = "triggers" + const val SCHEMA_VERSION_FIELD = "schema_version" + const val LOOK_BACK_WINDOW_FIELD = "look_back_window" + const val TIMESTAMP_FIELD = "timestamp_field" + + // default values + const val NO_ID = "" + const val NO_VERSION = 1L + + // hard, nonadjustable limits + const val MONITOR_V2_MAX_TRIGGERS = 10 + const val MONITOR_V2_MIN_LOOK_BACK_WINDOW = 1L // 1 minute + const val ALERTING_V2_MAX_NAME_LENGTH = 30 // max length of any name for monitors, triggers, notif actions, etc + const val UUID_LENGTH = 20 // the length of a UUID generated by UUIDs.base64UUID() + + val XCONTENT_REGISTRY = NamedXContentRegistry.Entry( + ScheduledJob::class.java, + ParseField(MONITOR_V2_TYPE), + CheckedFunction { parse(it) } + ) + + @JvmStatic + @Throws(IOException::class) + fun parse(xcp: XContentParser): MonitorV2 { + /* parse outer object for monitorV2 type, then delegate to correct monitorV2 parser */ + + XContentParserUtils.ensureExpectedToken( // outer monitor object start + XContentParser.Token.START_OBJECT, + xcp.currentToken(), + xcp + ) + + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) // monitor type field name + val monitorTypeText = xcp.currentName() + val monitorType = MonitorV2Type.enumFromString(monitorTypeText) + ?: throw IllegalStateException( + "when parsing MonitorV2, received invalid monitor type: $monitorTypeText. " + + "Please ensure monitor object is wrapped in an outer ppl_monitor object" + ) + + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner monitor object start + + return when (monitorType) { + MonitorV2Type.PPL_MONITOR -> PPLMonitor.parse(xcp) + } + } + + fun readFrom(sin: StreamInput): MonitorV2 { + return when (val monitorType = sin.readEnum(MonitorV2Type::class.java)) { + MonitorV2Type.PPL_MONITOR -> PPLMonitor(sin) + else -> throw IllegalStateException("Unexpected input \"$monitorType\" when reading MonitorV2") + } + } + + fun writeTo(out: StreamOutput, monitorV2: MonitorV2) { + when (monitorV2) { + is PPLMonitor -> { + out.writeEnum(MonitorV2Type.PPL_MONITOR) + monitorV2.writeTo(out) + } + } + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt new file mode 100644 index 000000000..cb36984ef --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.common.io.stream.Writeable +import org.opensearch.core.xcontent.ToXContent + +interface MonitorV2RunResult : Writeable, ToXContent { + val monitorName: String + val error: Exception? + val triggerResults: Map + + enum class MonitorV2RunResultType() { + PPL_MONITOR_RUN_RESULT; + } + + companion object { + const val ERROR_FIELD = "error" + const val TRIGGER_RESULTS_FIELD = "trigger_results" + + fun readFrom(sin: StreamInput): MonitorV2RunResult { + val monitorRunResultType = sin.readEnum(MonitorV2RunResultType::class.java) + return when (monitorRunResultType) { + MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT -> PPLMonitorRunResult(sin) + else -> throw IllegalStateException("Unexpected input [$monitorRunResultType] when reading MonitorV2RunResult") + } + } + + fun writeTo(out: StreamOutput, monitorV2RunResult: MonitorV2RunResult) { + when (monitorV2RunResult) { + is PPLMonitorRunResult -> { + out.writeEnum(MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT) + monitorV2RunResult.writeTo(out) + } + } + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt new file mode 100644 index 000000000..d5ac32837 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt @@ -0,0 +1,386 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.core.util.nonOptionalTimeField +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ALERTING_V2_MAX_NAME_LENGTH +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ENABLED_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ENABLED_TIME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.LOOK_BACK_WINDOW_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.MONITOR_V2_MAX_TRIGGERS +import org.opensearch.alerting.modelv2.MonitorV2.Companion.MONITOR_V2_MIN_LOOK_BACK_WINDOW +import org.opensearch.alerting.modelv2.MonitorV2.Companion.NAME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.NO_ID +import org.opensearch.alerting.modelv2.MonitorV2.Companion.NO_VERSION +import org.opensearch.alerting.modelv2.MonitorV2.Companion.SCHEDULE_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.SCHEMA_VERSION_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.TIMESTAMP_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.TRIGGERS_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.USER_FIELD +import org.opensearch.commons.alerting.model.CronSchedule +import org.opensearch.commons.alerting.model.Schedule +import org.opensearch.commons.alerting.util.AlertingException +import org.opensearch.commons.alerting.util.IndexUtils +import org.opensearch.commons.alerting.util.instant +import org.opensearch.commons.alerting.util.optionalTimeField +import org.opensearch.commons.alerting.util.optionalUserField +import org.opensearch.commons.authuser.User +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.core.xcontent.XContentBuilder +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import java.io.IOException +import java.time.Instant + +// TODO: eventually change this to be called PPLSQLMonitor. +// A PPL Monitor and SQL Monitor +// would have the exact same functionality, except the choice of language +// when calling PPL/SQL plugin's execute API would be different. +// we dont need 2 different monitor types for that, just a simple if check +// for query language at monitor execution time +/** + * PPL (Piped Processing Language) Monitor for OpenSearch Alerting V2 + * + * @property id Monitor ID. Defaults to [NO_ID]. + * @property version Version number of the monitor. Defaults to [NO_VERSION]. + * @property name Display name of the monitor. + * @property enabled Boolean flag indicating whether the monitor is currently on or off. + * @property schedule Defines when and how often the monitor should run. Can be a CRON or interval schedule. + * @property lookBackWindow How far back each Monitor execution's query should look back when searching data. + * @property lastUpdateTime Timestamp of the last update to this monitor. + * @property enabledTime Timestamp when the monitor was last enabled. Null if never enabled. + * @property triggers List of [PPLTrigger]s associated with this monitor. + * @property schemaVersion Version of the alerting-config index schema used when this Monitor was indexed. Defaults to [NO_SCHEMA_VERSION]. + * @property queryLanguage The query language used. Defaults to [QueryLanguage.PPL]. + * @property query The PPL query string to be executed by this monitor. + */ +data class PPLMonitor( + override val id: String = NO_ID, + override val version: Long = NO_VERSION, + override val name: String, + override val enabled: Boolean, + override val schedule: Schedule, + override val lookBackWindow: Long?, + override val timestampField: String?, + override val lastUpdateTime: Instant, + override val enabledTime: Instant?, + override val user: User?, + override val triggers: List, + override val schemaVersion: Int = IndexUtils.NO_SCHEMA_VERSION, + val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported + val query: String +) : MonitorV2 { + + // specify scheduled job type + override val type = MonitorV2.MONITOR_V2_TYPE + + override fun fromDocument(id: String, version: Long): PPLMonitor = copy(id = id, version = version) + + init { + // SQL monitors are not yet supported + if (queryLanguage == QueryLanguage.SQL) { + throw IllegalArgumentException("SQL queries are not supported. Please use a PPL query.") + } + + require(this.name.length <= ALERTING_V2_MAX_NAME_LENGTH) { + "Monitor name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" + } + + if (lookBackWindow != null) { + requireNotNull(timestampField) { "If look back window is specified, timestamp field must not be null" } + } else { + require(timestampField == null) { "If look back window is not specified, timestamp field must not be specified" } + } + + require(triggers.isNotEmpty()) { "Monitor must include at least 1 trigger" } + require(this.triggers.size <= MONITOR_V2_MAX_TRIGGERS) { "Monitors can only have $MONITOR_V2_MAX_TRIGGERS triggers" } + + lookBackWindow?.let { + require(this.lookBackWindow >= MONITOR_V2_MIN_LOOK_BACK_WINDOW) { + "Monitors look back windows must be at least $MONITOR_V2_MIN_LOOK_BACK_WINDOW minute" + } + } + + // for checking trigger ID uniqueness + val triggerIds = mutableSetOf() + this.triggers.forEach { trigger -> + require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique." } + } + + if (this.enabled) { + requireNotNull(this.enabledTime) + } else { + require(this.enabledTime == null) + } + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + id = sin.readString(), + version = sin.readLong(), + name = sin.readString(), + enabled = sin.readBoolean(), + schedule = Schedule.readFrom(sin), + lookBackWindow = sin.readOptionalLong(), + timestampField = sin.readOptionalString(), + lastUpdateTime = sin.readInstant(), + enabledTime = sin.readOptionalInstant(), + user = if (sin.readBoolean()) { + User(sin) + } else { + null + }, + triggers = sin.readList(PPLTrigger.Companion::readFrom), + schemaVersion = sin.readInt(), + queryLanguage = sin.readEnum(QueryLanguage::class.java), + query = sin.readString() + ) + + override fun toXContentWithUser(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + return createXContentBuilder(builder, params, true) + } + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + return createXContentBuilder(builder, params, false) + } + + private fun createXContentBuilder(builder: XContentBuilder, params: ToXContent.Params, withUser: Boolean): XContentBuilder { + builder.startObject() // overall start object + + // if this is being written as ScheduledJob, add extra object layer and add ScheduledJob + // related metadata, default to false + if (params.paramAsBoolean("with_type", false)) { + builder.startObject(MonitorV2.MONITOR_V2_TYPE) + } + + // wrap PPLMonitor in outer object named after its monitor type + // required for MonitorV2 XContentParser to first encounter this, + // read in monitor type, then delegate to correct parse() function + builder.startObject(PPL_MONITOR_TYPE) // monitor type start object + + builder.field(NAME_FIELD, name) + builder.field(SCHEDULE_FIELD, schedule) + builder.field(LOOK_BACK_WINDOW_FIELD, lookBackWindow) + builder.field(TIMESTAMP_FIELD, timestampField) + builder.field(ENABLED_FIELD, enabled) + builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) + builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) + builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) + builder.field(SCHEMA_VERSION_FIELD, schemaVersion) + builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) + builder.field(QUERY_FIELD, query) + + if (withUser) { + builder.optionalUserField(USER_FIELD, user) + } + + builder.endObject() // monitor type end object + + // if ScheduledJob metadata was added, end the extra object layer that was created + if (params.paramAsBoolean("with_type", false)) { + builder.endObject() + } + + builder.endObject() // overall end object + + return builder + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(id) + out.writeLong(version) + out.writeString(name) + out.writeBoolean(enabled) + + if (schedule is CronSchedule) { + out.writeEnum(Schedule.TYPE.CRON) + } else { + out.writeEnum(Schedule.TYPE.INTERVAL) + } + schedule.writeTo(out) + + out.writeOptionalLong(lookBackWindow) + out.writeOptionalString(timestampField) + out.writeInstant(lastUpdateTime) + out.writeOptionalInstant(enabledTime) + + out.writeBoolean(user != null) + user?.writeTo(out) + + out.writeVInt(triggers.size) + triggers.forEach { it.writeTo(out) } + out.writeInt(schemaVersion) + out.writeEnum(queryLanguage) + out.writeString(query) + } + + override fun asTemplateArg(): Map { + return mapOf( + IndexUtils._ID to id, + IndexUtils._VERSION to version, + NAME_FIELD to name, + ENABLED_FIELD to enabled, + SCHEDULE_FIELD to schedule, + LOOK_BACK_WINDOW_FIELD to lookBackWindow, + LAST_UPDATE_TIME_FIELD to lastUpdateTime.toEpochMilli(), + ENABLED_TIME_FIELD to enabledTime?.toEpochMilli(), + QUERY_FIELD to query + ) + } + + override fun makeCopy( + id: String, + version: Long, + name: String, + enabled: Boolean, + schedule: Schedule, + lastUpdateTime: Instant, + enabledTime: Instant?, + user: User?, + schemaVersion: Int, + lookBackWindow: Long?, + timestampField: String? + ): PPLMonitor { + return copy( + id = id, + version = version, + name = name, + enabled = enabled, + schedule = schedule, + lastUpdateTime = lastUpdateTime, + enabledTime = enabledTime, + user = user, + schemaVersion = schemaVersion, + lookBackWindow = lookBackWindow, + timestampField = timestampField + ) + } + + enum class QueryLanguage(val value: String) { + PPL(PPL_QUERY_LANGUAGE), + SQL(SQL_QUERY_LANGUAGE); + + companion object { + fun enumFromString(value: String): QueryLanguage? = QueryLanguage.entries.firstOrNull { it.value == value } + } + } + + companion object { + // monitor type name + const val PPL_MONITOR_TYPE = "ppl_monitor" // TODO: eventually change to SQL_PPL_MONITOR_TYPE + + // query languages + const val PPL_QUERY_LANGUAGE = "ppl" + const val SQL_QUERY_LANGUAGE = "sql" + + // field names + const val QUERY_LANGUAGE_FIELD = "query_language" + const val QUERY_FIELD = "query" + + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): PPLMonitor { + var name: String? = null + var enabled = true + var schedule: Schedule? = null + var lookBackWindow: Long? = null + var timestampField: String? = null + var lastUpdateTime: Instant? = null + var enabledTime: Instant? = null + var user: User? = null + val triggers: MutableList = mutableListOf() + var schemaVersion = IndexUtils.NO_SCHEMA_VERSION + var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL + var query: String? = null + + /* parse */ + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = xcp.currentName() + xcp.nextToken() + + when (fieldName) { + NAME_FIELD -> name = xcp.text() + ENABLED_FIELD -> enabled = xcp.booleanValue() + SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) + LOOK_BACK_WINDOW_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + lookBackWindow = xcp.longValue() + } + } + TIMESTAMP_FIELD -> timestampField = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) null else xcp.text() + LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() + ENABLED_TIME_FIELD -> enabledTime = xcp.instant() + USER_FIELD -> user = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) null else User.parse(xcp) + TRIGGERS_FIELD -> { + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_ARRAY, + xcp.currentToken(), + xcp + ) + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + triggers.add(PPLTrigger.parseInner(xcp)) + } + } + SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() + QUERY_LANGUAGE_FIELD -> { + val input = xcp.text() + val enumMatchResult = QueryLanguage.enumFromString(input) + ?: throw AlertingException.wrap( + IllegalArgumentException( + "Invalid value for $QUERY_LANGUAGE_FIELD: $input. " + + "Supported values are ${QueryLanguage.entries.map { it.value }}" + ) + ) + queryLanguage = enumMatchResult + } + QUERY_FIELD -> query = xcp.text() + else -> throw IllegalArgumentException("Unexpected field when parsing PPL Monitor: $fieldName") + } + } + + /* validations */ + + // if enabled, set time of MonitorV2 creation/update is set as enable time + if (enabled && enabledTime == null) { + enabledTime = Instant.now() + } else if (!enabled) { + enabledTime = null + } + + lastUpdateTime = lastUpdateTime ?: Instant.now() + + // check for required fields + requireNotNull(name) { "Monitor name is null" } + requireNotNull(schedule) { "Schedule is null" } + requireNotNull(query) { "Query is null" } + requireNotNull(lastUpdateTime) { "Last update time is null" } + + /* return PPLMonitor */ + return PPLMonitor( + id, + version, + name, + enabled, + schedule, + lookBackWindow, + timestampField, + lastUpdateTime, + enabledTime, + user, + triggers, + schemaVersion, + queryLanguage, + query + ) + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt new file mode 100644 index 000000000..5640c05fe --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.modelv2.AlertV2.Companion.MONITOR_V2_NAME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2RunResult.Companion.ERROR_FIELD +import org.opensearch.alerting.modelv2.MonitorV2RunResult.Companion.TRIGGER_RESULTS_FIELD +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.core.xcontent.XContentBuilder +import java.io.IOException + +data class PPLMonitorRunResult( + override val monitorName: String, + override val error: Exception?, + override val triggerResults: Map, + val pplQueryResults: Map> // key: trigger id, value: query results +) : MonitorV2RunResult { + + @Throws(IOException::class) + @Suppress("UNCHECKED_CAST") + constructor(sin: StreamInput) : this( + sin.readString(), // monitorName + sin.readException(), // error + sin.readMap() as Map, // triggerResults + sin.readMap() as Map> // pplQueryResults + ) + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + builder.field(MONITOR_V2_NAME_FIELD, monitorName) + builder.field(ERROR_FIELD, error?.message) + builder.field(TRIGGER_RESULTS_FIELD, triggerResults) + builder.field(PPL_QUERY_RESULTS_FIELD, pplQueryResults) + builder.endObject() + return builder + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(monitorName) + out.writeException(error) + out.writeMap(triggerResults) + out.writeMap(pplQueryResults) + } + + companion object { + const val PPL_QUERY_RESULTS_FIELD = "ppl_query_results" + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt new file mode 100644 index 000000000..92caf46e3 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt @@ -0,0 +1,397 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ALERTING_V2_MAX_NAME_LENGTH +import org.opensearch.alerting.modelv2.MonitorV2.Companion.UUID_LENGTH +import org.opensearch.alerting.modelv2.TriggerV2.Companion.ACTIONS_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.DEFAULT_EXPIRE_DURATION +import org.opensearch.alerting.modelv2.TriggerV2.Companion.EXPIRE_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.ID_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.LAST_TRIGGERED_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES +import org.opensearch.alerting.modelv2.TriggerV2.Companion.MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES +import org.opensearch.alerting.modelv2.TriggerV2.Companion.NAME_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.NOTIFICATIONS_ID_MAX_LENGTH +import org.opensearch.alerting.modelv2.TriggerV2.Companion.SEVERITY_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.THROTTLE_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Severity +import org.opensearch.common.CheckedFunction +import org.opensearch.common.UUIDs +import org.opensearch.commons.alerting.model.action.Action +import org.opensearch.commons.alerting.util.instant +import org.opensearch.commons.alerting.util.optionalTimeField +import org.opensearch.core.ParseField +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.core.xcontent.XContentBuilder +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import java.io.IOException +import java.time.Instant + +/** + * The PPL Trigger for PPL Monitors + * + * There are two types of PPLTrigger conditions: NUMBER_OF_RESULTS and CUSTOM + * NUMBER_OF_RESULTS: triggers based on whether the number of query results returned by the PPLMonitor + * query meets some threshold + * CUSTOM: triggers based on a custom condition that user specifies (a single ppl eval statement) + * + * PPLTriggers can run on two modes: RESULT_SET and PER_RESULT + * RESULT_SET: exactly one Alert is generated when the Trigger condition is met + * PER_RESULT: one Alert is generated per trigger condition-meeting query result row + * + * @property id Trigger ID, defaults to a base64 UUID. + * @property name Display name of the Trigger. + * @property severity The severity level of the Trigger. + * @property throttleDuration Optional duration (in minutes) for which alerts from this Trigger should be throttled/suppressed. + * Null indicates no throttling. + * @property expireDuration Duration (in minutes) after which alerts from this Trigger should be deleted permanently. + * @property lastTriggeredTime The last time this Trigger generated an Alert. Null if Trigger hasn't generated an Alert yet. + * @property actions List of notification-sending actions to run when the Trigger condition is met. + * @property mode Specifies whether the trigger evaluates the entire result set or each result individually. + * Can be either [TriggerMode.RESULT_SET] or [TriggerMode.PER_RESULT]. + * @property conditionType The type of condition to evaluate. + * Can be either [ConditionType.NUMBER_OF_RESULTS] or [ConditionType.CUSTOM]. + * @property numResultsCondition The comparison operator for NUMBER_OF_RESULTS conditions. Required if using NUMBER_OF_RESULTS conditions, + * required to be null otherwise. + * @property numResultsValue The threshold value for NUMBER_OF_RESULTS conditions. Required if using NUMBER_OF_RESULTS conditions, + * required to be null otherwise. + * @property customCondition A custom condition expression. Required if using CUSTOM conditions, + * required to be null otherwise. + */ +data class PPLTrigger( + override val id: String = UUIDs.base64UUID(), + override val name: String, + override val severity: Severity, + override val throttleDuration: Long?, + override val expireDuration: Long = DEFAULT_EXPIRE_DURATION, + override var lastTriggeredTime: Instant?, + override val actions: List, + val mode: TriggerMode, // RESULT_SET or PER_RESULT + val conditionType: ConditionType, // NUMBER_OF_RESULTS or CUSTOM + val numResultsCondition: NumResultsCondition?, + val numResultsValue: Long?, + val customCondition: String? +) : TriggerV2 { + + init { + requireNotNull(this.name) { "Trigger name must be included" } + requireNotNull(this.severity) { "Trigger severity must be included" } + requireNotNull(this.mode) { "Trigger mode must be included" } + requireNotNull(this.conditionType) { "Trigger condition type must be included" } + + require(this.id.length <= UUID_LENGTH) { + "Trigger ID too long, length must be less than $UUID_LENGTH" + } + + require(this.name.length <= ALERTING_V2_MAX_NAME_LENGTH) { + "Trigger name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" + } + + require(this.expireDuration >= MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES) { + "expire duration cannot be less than $MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES, was $expireDuration" + } + + this.throttleDuration?.let { + require(it >= MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES) { + "Throttle duration cannot be less than $MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES, was $throttleDuration" + } + } + + this.actions.forEach { + require(it.name.length <= ALERTING_V2_MAX_NAME_LENGTH) { + "Name of action with ID ${it.id} too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" + } + require(it.destinationId.length <= NOTIFICATIONS_ID_MAX_LENGTH) { + "Channel ID of action with ID ${it.id} too long, length must be less than $NOTIFICATIONS_ID_MAX_LENGTH" + } + } + + when (this.conditionType) { + ConditionType.NUMBER_OF_RESULTS -> { + requireNotNull(this.numResultsCondition) { + "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}," + + "$NUM_RESULTS_CONDITION_FIELD must be included" + } + requireNotNull(this.numResultsValue) { + "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}," + + "$NUM_RESULTS_VALUE_FIELD must be included" + } + require(this.customCondition == null) { + "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}," + + "$CUSTOM_CONDITION_FIELD must not be included" + } + } + ConditionType.CUSTOM -> { + requireNotNull(this.customCondition) { + "if trigger condition is of type ${ConditionType.CUSTOM.value}," + + "$CUSTOM_CONDITION_FIELD must be included" + } + require(this.numResultsCondition == null) { + "if trigger condition is of type ${ConditionType.CUSTOM.value}," + + "$NUM_RESULTS_CONDITION_FIELD must not be included" + } + require(this.numResultsValue == null) { + "if trigger condition is of type ${ConditionType.CUSTOM.value}," + + "$NUM_RESULTS_VALUE_FIELD must not be included" + } + } + } + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + sin.readString(), // id + sin.readString(), // name + sin.readEnum(Severity::class.java), // severity + sin.readOptionalLong(), // throttleDuration + sin.readLong(), // expireDuration + sin.readOptionalInstant(), // lastTriggeredTime + sin.readList(::Action), // actions + sin.readEnum(TriggerMode::class.java), // trigger mode + sin.readEnum(ConditionType::class.java), // condition type + if (sin.readBoolean()) sin.readEnum(NumResultsCondition::class.java) else null, // num results condition + sin.readOptionalLong(), // num results value + sin.readOptionalString() // custom condition + ) + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(id) + out.writeString(name) + out.writeEnum(severity) + out.writeOptionalLong(throttleDuration) + out.writeLong(expireDuration) + out.writeOptionalInstant(lastTriggeredTime) + out.writeCollection(actions) + out.writeEnum(mode) + out.writeEnum(conditionType) + + out.writeBoolean(numResultsCondition != null) + numResultsCondition?.let { out.writeEnum(numResultsCondition) } + + out.writeOptionalLong(numResultsValue) + out.writeOptionalString(customCondition) + } + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params?): XContentBuilder { + builder.startObject() + builder.field(ID_FIELD, id) + builder.field(NAME_FIELD, name) + builder.field(SEVERITY_FIELD, severity.value) + throttleDuration?.let { builder.field(THROTTLE_FIELD, throttleDuration) } + builder.field(EXPIRE_FIELD, expireDuration) + builder.optionalTimeField(LAST_TRIGGERED_FIELD, lastTriggeredTime) + builder.field(ACTIONS_FIELD, actions.toTypedArray()) + builder.field(MODE_FIELD, mode.value) + builder.field(CONDITION_TYPE_FIELD, conditionType.value) + numResultsCondition?.let { builder.field(NUM_RESULTS_CONDITION_FIELD, numResultsCondition.value) } + numResultsValue?.let { builder.field(NUM_RESULTS_VALUE_FIELD, numResultsValue) } + customCondition?.let { builder.field(CUSTOM_CONDITION_FIELD, customCondition) } + builder.endObject() + return builder + } + + fun asTemplateArg(): Map { + return mapOf( + ID_FIELD to id, + NAME_FIELD to name, + SEVERITY_FIELD to severity.value, + THROTTLE_FIELD to throttleDuration, + EXPIRE_FIELD to expireDuration, + ACTIONS_FIELD to actions.map { it.asTemplateArg() }, + MODE_FIELD to mode.value, + CONDITION_TYPE_FIELD to conditionType.value, + NUM_RESULTS_CONDITION_FIELD to numResultsCondition?.value, + NUM_RESULTS_VALUE_FIELD to numResultsValue, + CUSTOM_CONDITION_FIELD to customCondition + ) + } + + enum class TriggerMode(val value: String) { + RESULT_SET("result_set"), + PER_RESULT("per_result"); + + companion object { + fun enumFromString(value: String): TriggerMode? = entries.firstOrNull { it.value == value } + } + } + + enum class ConditionType(val value: String) { + NUMBER_OF_RESULTS("number_of_results"), + CUSTOM("custom"); + + companion object { + fun enumFromString(value: String): ConditionType? = entries.firstOrNull { it.value == value } + } + } + + enum class NumResultsCondition(val value: String) { + GREATER_THAN(">"), + GREATER_THAN_EQUAL(">="), + LESS_THAN("<"), + LESS_THAN_EQUAL("<="), + EQUAL("=="), + NOT_EQUAL("!="); + + companion object { + fun enumFromString(value: String): NumResultsCondition? = entries.firstOrNull { it.value == value } + } + } + + companion object { + // trigger wrapper object field name + const val PPL_TRIGGER_FIELD = "ppl_trigger" + + // field names + const val MODE_FIELD = "mode" + const val CONDITION_TYPE_FIELD = "type" + const val NUM_RESULTS_CONDITION_FIELD = "num_results_condition" + const val NUM_RESULTS_VALUE_FIELD = "num_results_value" + const val CUSTOM_CONDITION_FIELD = "custom_condition" + + val XCONTENT_REGISTRY = NamedXContentRegistry.Entry( + TriggerV2::class.java, + ParseField(PPL_TRIGGER_FIELD), + CheckedFunction { parseInner(it) } + ) + + @JvmStatic + @Throws(IOException::class) + fun parseInner(xcp: XContentParser): PPLTrigger { + var id = UUIDs.base64UUID() // assign a default triggerId if one is not specified + var name: String? = null + var severity: Severity? = null + var throttleDuration: Long? = null + var expireDuration: Long = DEFAULT_EXPIRE_DURATION + var lastTriggeredTime: Instant? = null + val actions: MutableList = mutableListOf() + var mode: TriggerMode? = null + var conditionType: ConditionType? = null + var numResultsCondition: NumResultsCondition? = null + var numResultsValue: Long? = null + var customCondition: String? = null + + /* parse */ + XContentParserUtils.ensureExpectedToken( // outer trigger object start + XContentParser.Token.START_OBJECT, + xcp.currentToken(), xcp + ) + + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = xcp.currentName() + xcp.nextToken() + + when (fieldName) { + ID_FIELD -> id = xcp.text() + NAME_FIELD -> name = xcp.text() + SEVERITY_FIELD -> { + val input = xcp.text() + val enumMatchResult = Severity.enumFromString(input) + ?: throw IllegalArgumentException( + "Invalid value for $SEVERITY_FIELD: $input. " + + "Supported values are ${Severity.entries.map { it.value }}" + ) + severity = enumMatchResult + } + MODE_FIELD -> { + val input = xcp.text() + val enumMatchResult = TriggerMode.enumFromString(input) + ?: throw IllegalArgumentException( + "Invalid value for $MODE_FIELD: $input. " + + "Supported values are ${TriggerMode.entries.map { it.value }}" + ) + mode = enumMatchResult + } + CONDITION_TYPE_FIELD -> { + val input = xcp.text() + val enumMatchResult = ConditionType.enumFromString(input) + ?: throw IllegalArgumentException( + "Invalid value for $CONDITION_TYPE_FIELD: $input. " + + "Supported values are ${ConditionType.entries.map { it.value }}" + ) + conditionType = enumMatchResult + } + NUM_RESULTS_CONDITION_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + val input = xcp.text() + val enumMatchResult = NumResultsCondition.enumFromString(input) + ?: throw IllegalArgumentException( + "Invalid value for $NUM_RESULTS_CONDITION_FIELD: $input. " + + "Supported values are ${NumResultsCondition.entries.map { it.value }}" + ) + numResultsCondition = enumMatchResult + } + } + NUM_RESULTS_VALUE_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + numResultsValue = xcp.longValue() + } + } + CUSTOM_CONDITION_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + customCondition = xcp.text() + } + } + THROTTLE_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + throttleDuration = xcp.longValue() + } + } + EXPIRE_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + expireDuration = xcp.longValue() + } + } + LAST_TRIGGERED_FIELD -> lastTriggeredTime = xcp.instant() + ACTIONS_FIELD -> { + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_ARRAY, + xcp.currentToken(), + xcp + ) + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + actions.add(Action.parse(xcp)) + } + } + else -> throw IllegalArgumentException("Unexpected field when parsing PPL Trigger: $fieldName") + } + } + + /* validations */ + requireNotNull(name) { "Trigger name must be included" } + requireNotNull(severity) { "Trigger severity must be included" } + requireNotNull(mode) { "Trigger mode must be included" } + requireNotNull(conditionType) { "Trigger condition type must be included" } + + // 3. prepare and return PPLTrigger object + return PPLTrigger( + id, + name, + severity, + throttleDuration, + expireDuration, + lastTriggeredTime, + actions, + mode, + conditionType, + numResultsCondition, + numResultsValue, + customCondition + ) + } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): PPLTrigger { + return PPLTrigger(sin) + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt new file mode 100644 index 000000000..a7a6076a4 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.modelv2.TriggerV2RunResult.Companion.ERROR_FIELD +import org.opensearch.alerting.modelv2.TriggerV2RunResult.Companion.NAME_FIELD +import org.opensearch.alerting.modelv2.TriggerV2RunResult.Companion.TRIGGERED_FIELD +import org.opensearch.commons.alerting.model.QueryLevelTriggerRunResult +import org.opensearch.commons.alerting.model.TriggerRunResult +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.core.xcontent.XContentBuilder +import java.io.IOException + +data class PPLTriggerRunResult( + override var triggerName: String, + override var triggered: Boolean, + override var error: Exception?, +) : TriggerV2RunResult { + + @Throws(IOException::class) + @Suppress("UNCHECKED_CAST") + constructor(sin: StreamInput) : this( + triggerName = sin.readString(), + triggered = sin.readBoolean(), + error = sin.readException() + ) + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + builder.field(NAME_FIELD, triggerName) + builder.field(TRIGGERED_FIELD, triggered) + builder.field(ERROR_FIELD, error?.message) + builder.endObject() + return builder + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(triggerName) + out.writeBoolean(triggered) + out.writeException(error) + } + + companion object { + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): TriggerRunResult { + return QueryLevelTriggerRunResult(sin) + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt new file mode 100644 index 000000000..c54d3771e --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.modelv2.PPLTrigger.Companion.PPL_TRIGGER_FIELD +import org.opensearch.commons.alerting.model.action.Action +import org.opensearch.commons.notifications.model.BaseModel +import java.time.Instant + +interface TriggerV2 : BaseModel { + + val id: String + val name: String + val severity: Severity + val throttleDuration: Long? + val expireDuration: Long + var lastTriggeredTime: Instant? + val actions: List + + enum class TriggerV2Type(val value: String) { + PPL_TRIGGER(PPL_TRIGGER_FIELD); + + override fun toString(): String { + return value + } + } + + enum class Severity(val value: String) { + INFO("info"), + ERROR("error"), + LOW("low"), + MEDIUM("medium"), + HIGH("high"), + CRITICAL("critical"); + + companion object { + fun enumFromString(value: String): Severity? { + return entries.find { it.value == value } + } + } + } + + companion object { + // field names + const val ID_FIELD = "id" + const val NAME_FIELD = "name" + const val SEVERITY_FIELD = "severity" + const val THROTTLE_FIELD = "throttle" + const val LAST_TRIGGERED_FIELD = "last_triggered_time" + const val EXPIRE_FIELD = "expires" + const val ACTIONS_FIELD = "actions" + + // hard, nonadjustable limits + const val MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES = 1L // one minute min duration to match scheduled job interval granularity + const val MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES = 1L // one minute min duration to match scheduled job interval granularity + const val NOTIFICATIONS_ID_MAX_LENGTH = 512 // length limit for notifications channel custom ID at channel creation time + + // default fallback values of fields if none are passed in + const val DEFAULT_EXPIRE_DURATION = (7 * 24 * 60).toLong() // 7 days in minutes + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2RunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2RunResult.kt new file mode 100644 index 000000000..5a09e4b7c --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2RunResult.kt @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.core.common.io.stream.Writeable +import org.opensearch.core.xcontent.ToXContent + +interface TriggerV2RunResult : Writeable, ToXContent { + + val triggerName: String + val triggered: Boolean + val error: Exception? + + companion object { + const val NAME_FIELD = "name" + const val TRIGGERED_FIELD = "triggered" + const val ERROR_FIELD = "error" + } +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index 61276abde..fefb64c70 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -12,6 +12,14 @@ import org.opensearch.alerting.model.AlertContext import org.opensearch.alerting.model.destination.email.EmailAccount import org.opensearch.alerting.model.destination.email.EmailEntry import org.opensearch.alerting.model.destination.email.EmailGroup +import org.opensearch.alerting.modelv2.AlertV2 +import org.opensearch.alerting.modelv2.PPLMonitor +import org.opensearch.alerting.modelv2.PPLMonitor.QueryLanguage +import org.opensearch.alerting.modelv2.PPLTrigger +import org.opensearch.alerting.modelv2.PPLTrigger.ConditionType +import org.opensearch.alerting.modelv2.PPLTrigger.NumResultsCondition +import org.opensearch.alerting.modelv2.PPLTrigger.TriggerMode +import org.opensearch.alerting.modelv2.TriggerV2.Severity import org.opensearch.alerting.util.getBucketKeysHash import org.opensearch.client.Request import org.opensearch.client.RequestOptions @@ -79,10 +87,18 @@ import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.test.OpenSearchTestCase.randomBoolean import org.opensearch.test.OpenSearchTestCase.randomInt import org.opensearch.test.OpenSearchTestCase.randomIntBetween +import org.opensearch.test.OpenSearchTestCase.randomLongBetween import org.opensearch.test.rest.OpenSearchRestTestCase +import org.opensearch.test.rest.OpenSearchRestTestCase.assertEquals import java.time.Instant import java.time.temporal.ChronoUnit +// constants for PPL Alerting tests +const val TIMESTAMP_FIELD = "timestamp" +const val TEST_INDEX_NAME = "index" +const val TEST_INDEX_MAPPINGS = + """"properties":{"timestamp":{"type":"date"},"abc":{"type":"keyword"},"number":{"type":"integer"}}""" + fun randomQueryLevelMonitor( name: String = OpenSearchRestTestCase.randomAlphaOfLength(10), user: User = randomUser(), @@ -292,6 +308,34 @@ fun randomWorkflowWithDelegates( ) } +fun randomPPLMonitor( + name: String = OpenSearchRestTestCase.randomAlphaOfLength(10), + enabled: Boolean = randomBoolean(), + schedule: Schedule = IntervalSchedule(interval = 5, unit = ChronoUnit.MINUTES), + lookBackWindow: Long? = randomLongBetween(1, 100), + timestampField: String? = lookBackWindow?.let { TIMESTAMP_FIELD }, + lastUpdateTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), + enabledTime: Instant? = if (enabled) Instant.now().truncatedTo(ChronoUnit.MILLIS) else null, + triggers: List = List(randomIntBetween(1, 5)) { randomPPLTrigger() }, + user: User? = randomUser(), + queryLanguage: QueryLanguage = QueryLanguage.PPL, + query: String = "source = $TEST_INDEX_NAME | head 10" +): PPLMonitor { + return PPLMonitor( + name = name, + enabled = enabled, + schedule = schedule, + lookBackWindow = lookBackWindow, + timestampField = timestampField, + lastUpdateTime = lastUpdateTime, + enabledTime = enabledTime, + triggers = triggers, + user = user, + queryLanguage = queryLanguage, + query = query + ) +} + fun randomQueryLevelTrigger( id: String = UUIDs.base64UUID(), name: String = OpenSearchRestTestCase.randomAlphaOfLength(10), @@ -348,6 +392,42 @@ fun randomDocumentLevelTrigger( ) } +// random PPLTrigger defaults to a number_of_results trigger, because a custom condition +// would require knowledge of the PPL Monitor's query +// it is on the caller to be explicit and pass in valid arguments that would create either +// a valid PPL Monitor or one that intentionally throws an error. +// e.g. to create a valid PPL Monitor, if conditionType is CUSTOM, +// numResultsCondition and numResultsValue must be null, while +// customCondition must not be null. +fun randomPPLTrigger( + id: String = UUIDs.base64UUID(), + name: String = OpenSearchRestTestCase.randomAlphaOfLength(10), + severity: Severity = Severity.entries.random(), + throttleDuration: Long? = randomLongBetween(1, 100), + expireDuration: Long = randomLongBetween(1, 100), + actions: List = mutableListOf(), + mode: TriggerMode = TriggerMode.entries.random(), + conditionType: ConditionType = ConditionType.NUMBER_OF_RESULTS, + numResultsCondition: NumResultsCondition? = NumResultsCondition.entries.random(), + numResultsValue: Long? = randomLongBetween(1L, 50L), + customCondition: String? = null +): PPLTrigger { + return PPLTrigger( + id = id, + name = name, + severity = severity, + throttleDuration = throttleDuration, + expireDuration = expireDuration, + lastTriggeredTime = null, + actions = actions, + mode = mode, + conditionType = conditionType, + numResultsCondition = numResultsCondition, + numResultsValue = numResultsValue, + customCondition = customCondition + ) +} + fun randomBucketSelectorExtAggregationBuilder( name: String = OpenSearchRestTestCase.randomAlphaOfLength(10), bucketsPathsMap: MutableMap = mutableMapOf("avg" to "10"), @@ -424,10 +504,11 @@ fun randomTemplateScript( fun randomAction( name: String = OpenSearchRestTestCase.randomUnicodeOfLength(10), template: Script = randomTemplateScript("Hello World"), + subjectTemplate: Script = template, destinationId: String = "", throttleEnabled: Boolean = false, throttle: Throttle = randomThrottle() -) = Action(name, destinationId, template, template, throttleEnabled, throttle, actionExecutionPolicy = null) +) = Action(name, destinationId, subjectTemplate, template, throttleEnabled, throttle, actionExecutionPolicy = null) fun randomActionWithPolicy( name: String = OpenSearchRestTestCase.randomUnicodeOfLength(10), @@ -472,6 +553,62 @@ fun randomAlert(monitor: Monitor = randomQueryLevelMonitor()): Alert { ) } +/* +val id: String = NO_ID, +val version: Long = NO_VERSION, +val schemaVersion: Int = NO_SCHEMA_VERSION, +val monitorId: String, +val monitorName: String, +val monitorVersion: Long, +val monitorUser: User?, +val triggerId: String, +val triggerName: String, +val query: String, +val queryResults: Map, +val triggeredTime: Instant, +val expirationTime: Instant, +val errorMessage: String? = null, +val severity: Severity, +val executionId: String? = null + */ +fun randomAlertV2( + id: String = UUIDs.base64UUID(), + version: Long = randomLongBetween(1, 10), + schemaVersion: Int = randomIntBetween(1, 10), + monitorId: String = UUIDs.base64UUID(), + monitorName: String = UUIDs.base64UUID(), + monitorVersion: Long = randomLongBetween(1, 10), + monitorUser: User? = randomUser(), + triggerId: String = UUIDs.base64UUID(), + triggerName: String = UUIDs.base64UUID(), + query: String = "source = $TEST_INDEX_NAME | head 10", + queryResults: Map = mapOf(), + triggeredTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), + expirationTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorMessage: String? = "sample error message", + severity: Severity = Severity.entries.random(), + executionId: String? = UUIDs.base64UUID() +): AlertV2 { + return AlertV2( + id = id, + version = version, + schemaVersion = schemaVersion, + monitorId = monitorId, + monitorName = monitorName, + monitorVersion = monitorVersion, + monitorUser = monitorUser, + triggerId = triggerId, + triggerName = triggerName, + query = query, + queryResults = queryResults, + triggeredTime = triggeredTime, + expirationTime = expirationTime, + errorMessage = errorMessage, + severity = severity, + executionId = executionId, + ) +} + fun randomDocLevelQuery( id: String = OpenSearchRestTestCase.randomAlphaOfLength(10), query: String = OpenSearchRestTestCase.randomAlphaOfLength(10), @@ -810,3 +947,166 @@ fun randomAlertContext( fun Map.objectMap(key: String): Map> { return this[key] as Map> } + +fun assertPplMonitorsEqual(pplMonitor1: PPLMonitor, pplMonitor2: PPLMonitor) { + // note: Get and Search Monitor responses do not include User information by + // design, so that check is skipped + + // note: Update Monitor API intentionally overrides the enabledTime of the new given monitor + // with the enabledTime of the existing monitor being updated to ensure execution correctness, + // so that check is skipped + + assertEquals("Monitor enabled fields not equal", pplMonitor1.enabled, pplMonitor2.enabled) + assertEquals("Monitor schedules not equal", pplMonitor1.schedule, pplMonitor2.schedule) + assertEquals("Monitor lookback windows not equal", pplMonitor1.lookBackWindow, pplMonitor2.lookBackWindow) + assertEquals("Monitor timestamp fields not equal", pplMonitor1.timestampField, pplMonitor2.timestampField) + assertEquals("Monitor last updated times are not equal", pplMonitor1.lastUpdateTime, pplMonitor2.lastUpdateTime) + assertEquals("Monitor query languages not equal", pplMonitor1.queryLanguage, pplMonitor2.queryLanguage) + assertEquals("Monitor queries not equal", pplMonitor1.query, pplMonitor2.query) + assertEquals("Number of triggers in monitor not equal", pplMonitor1.triggers.size, pplMonitor2.triggers.size) + + val sortedTriggers1 = pplMonitor1.triggers.sortedBy { it.id } + val sortedTriggers2 = pplMonitor2.triggers.sortedBy { it.id } + for (i in sortedTriggers1.indices) { + assertPplTriggersEqual(sortedTriggers1[i], sortedTriggers2[i]) + } +} + +fun assertPplTriggersEqual(pplTrigger1: PPLTrigger, pplTrigger2: PPLTrigger) { + assertEquals( + "Monitor trigger IDs not equal", + pplTrigger1.id, + pplTrigger2.id + ) + + val id = pplTrigger1.id + + assertEquals( + "Monitor trigger $id names not equal", + pplTrigger1.name, + pplTrigger2.name + ) + assertEquals( + "Monitor trigger $id severities not equal", + pplTrigger1.severity, + pplTrigger2.severity + ) + assertEquals( + "Monitor trigger $id throttle durations not equal", + pplTrigger1.throttleDuration, + pplTrigger2.throttleDuration + ) + assertEquals( + "Monitor trigger $id expire durations not equal", + pplTrigger1.expireDuration, + pplTrigger2.expireDuration + ) + assertEquals( + "Monitor trigger $id modes not equal", + pplTrigger1.mode, + pplTrigger2.mode + ) + assertEquals( + "Monitor trigger $id condition types not equal", + pplTrigger1.conditionType, + pplTrigger2.conditionType + ) + assertEquals( + "Monitor trigger $id number_of_results conditions not equal", + pplTrigger1.numResultsCondition, + pplTrigger2.numResultsCondition + ) + assertEquals( + "Monitor trigger $id number_of_results values not equal", + pplTrigger1.numResultsValue, + pplTrigger2.numResultsValue + ) + assertEquals( + "Monitor trigger $id custom conditions not equal", + pplTrigger1.customCondition, + pplTrigger2.customCondition + ) +} + +fun assertAlertV2sEqual(alert1: AlertV2, alert2: AlertV2) { + assertEquals( + "AlertV2 IDs are not equal", + alert1.id, + alert2.id + ) + assertEquals( + "AlertV2 versions are not equal", + alert1.version, + alert2.version + ) + assertEquals( + "AlertV2 schema versions are not equal", + alert1.schemaVersion, + alert2.schemaVersion + ) + assertEquals( + "AlertV2 monitor IDs are not equal", + alert1.monitorId, + alert2.monitorId + ) + assertEquals( + "AlertV2 monitor names are not equal", + alert1.monitorName, + alert2.monitorName + ) + assertEquals( + "AlertV2 monitor versions are not equal", + alert1.monitorVersion, + alert2.monitorVersion + ) + assertEquals( + "AlertV2 monitor users are not equal", + alert1.monitorUser.toString(), + alert2.monitorUser.toString() + ) + assertEquals( + "AlertV2 trigger IDs are not equal", + alert1.triggerId, + alert2.triggerId + ) + assertEquals( + "AlertV2 trigger names are not equal", + alert1.triggerName, + alert2.triggerName + ) + assertEquals( + "AlertV2 queries are not equal", + alert1.query, + alert2.query + ) + assertEquals( + "AlertV2 query results are not equal", + alert1.queryResults, + alert2.queryResults + ) + assertEquals( + "AlertV2 triggered times are not equal", + alert1.triggeredTime, + alert2.triggeredTime + ) + assertEquals( + "AlertV2 expiration times are not equal", + alert1.expirationTime, + alert2.expirationTime + ) + assertEquals( + "AlertV2 error messages are not equal", + alert1.errorMessage, + alert2.errorMessage + ) + assertEquals( + "AlertV2 severities are not equal", + alert1.severity, + alert2.severity + ) + assertEquals( + "AlertV2 execution IDs are not equal", + alert1.executionId, + alert2.executionId + ) +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt new file mode 100644 index 000000000..6b40801e4 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.assertAlertV2sEqual +import org.opensearch.alerting.modelv2.AlertV2.Companion.ALERT_V2_ID_FIELD +import org.opensearch.alerting.modelv2.AlertV2.Companion.ALERT_V2_VERSION_FIELD +import org.opensearch.alerting.modelv2.AlertV2.Companion.ERROR_MESSAGE_FIELD +import org.opensearch.alerting.modelv2.AlertV2.Companion.EXECUTION_ID_FIELD +import org.opensearch.alerting.modelv2.AlertV2.Companion.EXPIRATION_TIME_FIELD +import org.opensearch.alerting.modelv2.AlertV2.Companion.SEVERITY_FIELD +import org.opensearch.alerting.randomAlertV2 +import org.opensearch.common.io.stream.BytesStreamOutput +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.test.OpenSearchTestCase + +class AlertV2Tests : OpenSearchTestCase() { + fun `test alertv2 as stream`() { + val alertV2 = randomAlertV2() + val out = BytesStreamOutput() + alertV2.writeTo(out) + val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) + val newAlertV2 = AlertV2(sin) + assertAlertV2sEqual(alertV2, newAlertV2) + } + + fun `test alertv2 asTemplateArgs`() { + val alertV2 = randomAlertV2() + val templateArgs = alertV2.asTemplateArg() + + assertEquals( + "Template args field $ALERT_V2_ID_FIELD doesn't match", + alertV2.id, + templateArgs[ALERT_V2_ID_FIELD] + ) + assertEquals( + "Template args field $ALERT_V2_VERSION_FIELD doesn't match", + alertV2.version, + templateArgs[ALERT_V2_VERSION_FIELD] + ) + assertEquals( + "Template args field $ERROR_MESSAGE_FIELD doesn't match", + alertV2.errorMessage, + templateArgs[ERROR_MESSAGE_FIELD] + ) + assertEquals( + "Template args field $EXECUTION_ID_FIELD doesn't match", + alertV2.executionId, + templateArgs[EXECUTION_ID_FIELD] + ) + assertEquals( + "Template args field $EXPIRATION_TIME_FIELD doesn't match", + alertV2.expirationTime.toEpochMilli(), + templateArgs[EXPIRATION_TIME_FIELD] + ) + assertEquals( + "Template args field $SEVERITY_FIELD doesn't match", + alertV2.severity.value, + templateArgs[SEVERITY_FIELD] + ) + } +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt new file mode 100644 index 000000000..274a96899 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt @@ -0,0 +1,161 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.assertPplMonitorsEqual +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ALERTING_V2_MAX_NAME_LENGTH +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ENABLED_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ENABLED_TIME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.LOOK_BACK_WINDOW_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.MONITOR_V2_MIN_LOOK_BACK_WINDOW +import org.opensearch.alerting.modelv2.MonitorV2.Companion.NAME_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.SCHEDULE_FIELD +import org.opensearch.alerting.modelv2.PPLMonitor.Companion.QUERY_FIELD +import org.opensearch.alerting.randomPPLMonitor +import org.opensearch.alerting.randomPPLTrigger +import org.opensearch.common.io.stream.BytesStreamOutput +import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID +import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.test.OpenSearchTestCase +import java.lang.IllegalArgumentException +import java.time.Instant + +class MonitorV2Tests : OpenSearchTestCase() { + fun `test enabled time`() { + val pplMonitor = randomPPLMonitor(enabled = true, enabledTime = Instant.now()) + try { + pplMonitor.makeCopy(enabled = false) + fail("Disabling monitor with enabled time set should fail.") + } catch (_: IllegalArgumentException) {} + + val disabledMonitor = pplMonitor.copy(enabled = false, enabledTime = null) + + try { + disabledMonitor.makeCopy(enabled = true) + fail("Enabling monitor without enabled time should fail") + } catch (_: IllegalArgumentException) {} + } + + fun `test max triggers`() { + val tooManyTriggers = mutableListOf() + for (i in 0..10) { // 11 times + tooManyTriggers.add(randomPPLTrigger()) + } + + try { + randomPPLMonitor(triggers = tooManyTriggers) + fail("Monitor with too many triggers should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test monitor name too long`() { + var monitorName = "" + for (i in 0 until ALERTING_V2_MAX_NAME_LENGTH + 1) { + monitorName += "a" + } + + try { + randomPPLMonitor(name = monitorName) + fail("Monitor with too long a name should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test monitor min look back window`() { + try { + randomPPLMonitor( + lookBackWindow = MONITOR_V2_MIN_LOOK_BACK_WINDOW - 1 + ) + fail("Monitor with too long a name should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test monitor no triggers`() { + try { + randomPPLMonitor( + triggers = listOf() + ) + fail("Monitor without triggers be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test monitor with look back window without timestamp field`() { + try { + randomPPLMonitor( + lookBackWindow = randomLongBetween(1, 10), + timestampField = null + ) + fail("Monitor with look back window but without timestamp field be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test monitor without look back window with timestamp field`() { + try { + randomPPLMonitor( + lookBackWindow = null, + timestampField = "some_timestamp_field" + ) + fail("Monitor without look back window but with timestamp field be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test ppl monitor as stream`() { + val pplMonitor = randomPPLMonitor() + val out = BytesStreamOutput() + pplMonitor.writeTo(out) + val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) + val newPplMonitor = PPLMonitor(sin) + assertPplMonitorsEqual(pplMonitor, newPplMonitor) + } + + fun `test ppl monitor asTemplateArgs`() { + val pplMonitor = randomPPLMonitor() + val templateArgs = pplMonitor.asTemplateArg() + + assertEquals( + "Template args field $_ID doesn't match", + pplMonitor.id, + templateArgs[_ID] + ) + assertEquals( + "Template args field $_VERSION doesn't match", + pplMonitor.version, + templateArgs[_VERSION] + ) + assertEquals( + "Template args field $NAME_FIELD doesn't match", + pplMonitor.name, + templateArgs[NAME_FIELD] + ) + assertEquals( + "Template args field $ENABLED_FIELD doesn't match", + pplMonitor.enabled, + templateArgs[ENABLED_FIELD] + ) + assertNotNull(templateArgs[SCHEDULE_FIELD]) + assertEquals( + "Template args field $LOOK_BACK_WINDOW_FIELD doesn't match", + pplMonitor.lookBackWindow, + templateArgs[LOOK_BACK_WINDOW_FIELD] + ) + assertEquals( + "Template args field $LAST_UPDATE_TIME_FIELD doesn't match", + pplMonitor.lastUpdateTime.toEpochMilli(), + templateArgs[LAST_UPDATE_TIME_FIELD] + ) + assertEquals( + "Template args field $ENABLED_TIME_FIELD doesn't match", + pplMonitor.enabledTime?.toEpochMilli(), + templateArgs[ENABLED_TIME_FIELD] + ) + assertEquals( + "Template args field $QUERY_FIELD doesn't match", + pplMonitor.query, + templateArgs[QUERY_FIELD] + ) + } +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt new file mode 100644 index 000000000..77fc19823 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt @@ -0,0 +1,243 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.modelv2 + +import org.opensearch.alerting.assertPplTriggersEqual +import org.opensearch.alerting.modelv2.MonitorV2.Companion.ALERTING_V2_MAX_NAME_LENGTH +import org.opensearch.alerting.modelv2.PPLTrigger.Companion.CONDITION_TYPE_FIELD +import org.opensearch.alerting.modelv2.PPLTrigger.Companion.CUSTOM_CONDITION_FIELD +import org.opensearch.alerting.modelv2.PPLTrigger.Companion.MODE_FIELD +import org.opensearch.alerting.modelv2.PPLTrigger.Companion.NUM_RESULTS_CONDITION_FIELD +import org.opensearch.alerting.modelv2.PPLTrigger.Companion.NUM_RESULTS_VALUE_FIELD +import org.opensearch.alerting.modelv2.PPLTrigger.ConditionType +import org.opensearch.alerting.modelv2.PPLTrigger.NumResultsCondition +import org.opensearch.alerting.modelv2.TriggerV2.Companion.ACTIONS_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.EXPIRE_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.ID_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES +import org.opensearch.alerting.modelv2.TriggerV2.Companion.MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES +import org.opensearch.alerting.modelv2.TriggerV2.Companion.NAME_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.NOTIFICATIONS_ID_MAX_LENGTH +import org.opensearch.alerting.modelv2.TriggerV2.Companion.SEVERITY_FIELD +import org.opensearch.alerting.modelv2.TriggerV2.Companion.THROTTLE_FIELD +import org.opensearch.alerting.randomAction +import org.opensearch.alerting.randomPPLTrigger +import org.opensearch.common.io.stream.BytesStreamOutput +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.test.OpenSearchTestCase +import java.lang.IllegalArgumentException + +class TriggerV2Tests : OpenSearchTestCase() { + fun `test min throttle duration`() { + try { + randomPPLTrigger( + throttleDuration = MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES - 1 + ) + fail("Trigger with throttle duration less than 1 should be rejected") + } catch (_: IllegalArgumentException) {} + } + + fun `test min expire duration`() { + try { + randomPPLTrigger( + expireDuration = MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES - 1 + ) + fail("Trigger with expire duration less than 1 should be rejected") + } catch (_: IllegalArgumentException) {} + } + + fun `test trigger name too long`() { + var triggerName = "" + for (i in 0 until ALERTING_V2_MAX_NAME_LENGTH + 1) { + triggerName += "a" + } + + try { + randomPPLTrigger(name = triggerName) + fail("Trigger with too long a name should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test trigger action name too long`() { + var actionName = "" + for (i in 0 until ALERTING_V2_MAX_NAME_LENGTH + 1) { + actionName += "a" + } + + try { + randomPPLTrigger( + actions = listOf( + randomAction( + name = actionName + ) + ) + ) + fail("Trigger action with too long a name should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test trigger action channel ID too long`() { + var channelId = "" + for (i in 0 until NOTIFICATIONS_ID_MAX_LENGTH + 1) { + channelId += "a" + } + + try { + randomPPLTrigger( + actions = listOf( + randomAction( + destinationId = channelId + ) + ) + ) + fail("Trigger action with too long a channel ID should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test number_of_results trigger has no number_of_results value field`() { + try { + randomPPLTrigger( + conditionType = ConditionType.NUMBER_OF_RESULTS, + numResultsCondition = NumResultsCondition.entries.random(), + numResultsValue = null, + customCondition = null + ) + fail("Number of results trigger that has no number of results value should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test number_of_results trigger has no number_of_results condition field`() { + try { + randomPPLTrigger( + conditionType = ConditionType.NUMBER_OF_RESULTS, + numResultsCondition = null, + numResultsValue = randomLongBetween(1, 10), + customCondition = null + ) + fail("Number of results trigger that has no number of results condition should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test number_of_results trigger has custom_condition value field`() { + try { + randomPPLTrigger( + conditionType = ConditionType.NUMBER_OF_RESULTS, + numResultsCondition = null, + numResultsValue = null, + customCondition = "eval result = something > 5" + ) + fail("Number of results trigger that has custom condition should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test custom trigger has number_of_results value field`() { + try { + randomPPLTrigger( + conditionType = ConditionType.CUSTOM, + numResultsCondition = NumResultsCondition.entries.random(), + numResultsValue = null, + customCondition = null + ) + fail("Number of results trigger that has no number of results value should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test custom trigger has number_of_results condition field`() { + try { + randomPPLTrigger( + conditionType = ConditionType.CUSTOM, + numResultsCondition = null, + numResultsValue = randomLongBetween(1, 10), + customCondition = null + ) + fail("Number of results trigger that has no number of results condition should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test custom trigger has no custom_condition value field`() { + try { + randomPPLTrigger( + conditionType = ConditionType.CUSTOM, + numResultsCondition = null, + numResultsValue = null, + customCondition = null + ) + fail("Number of results trigger that has custom condition should be rejected.") + } catch (_: IllegalArgumentException) {} + } + + fun `test ppl trigger as stream`() { + val pplTrigger = randomPPLTrigger() + val out = BytesStreamOutput() + pplTrigger.writeTo(out) + val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) + val newPplTrigger = PPLTrigger(sin) + assertPplTriggersEqual(pplTrigger, newPplTrigger) + } + + fun `test ppl trigger asTemplateArgs`() { + val pplTrigger = randomPPLTrigger() + val templateArgs = pplTrigger.asTemplateArg() + + assertEquals( + "Template args field $ID_FIELD doesn't match", + pplTrigger.id, + templateArgs[ID_FIELD] + ) + assertEquals( + "Template args field $NAME_FIELD doesn't match", + pplTrigger.name, + templateArgs[NAME_FIELD] + ) + assertEquals( + "Template args field $SEVERITY_FIELD doesn't match", + pplTrigger.severity.value, + templateArgs[SEVERITY_FIELD] + ) + assertEquals( + "Template args field $THROTTLE_FIELD doesn't match", + pplTrigger.throttleDuration, + templateArgs[THROTTLE_FIELD] + ) + assertEquals( + "Template args field $EXPIRE_FIELD doesn't match", + pplTrigger.expireDuration, + templateArgs[EXPIRE_FIELD] + ) + assertEquals( + "Template args field $EXPIRE_FIELD doesn't match", + pplTrigger.expireDuration, + templateArgs[EXPIRE_FIELD] + ) + val actions = templateArgs[ACTIONS_FIELD] as List<*> + assertEquals("number of trigger actions doesn't match", pplTrigger.actions.size, actions.size) + assertEquals( + "Template args field $MODE_FIELD doesn't match", + pplTrigger.mode.value, + templateArgs[MODE_FIELD] + ) + assertEquals( + "Template args field $CONDITION_TYPE_FIELD doesn't match", + pplTrigger.conditionType.value, + templateArgs[CONDITION_TYPE_FIELD] + ) + assertEquals( + "Template args field $NUM_RESULTS_CONDITION_FIELD doesn't match", + pplTrigger.numResultsCondition?.value, + templateArgs[NUM_RESULTS_CONDITION_FIELD] + ) + assertEquals( + "Template args field $NUM_RESULTS_VALUE_FIELD doesn't match", + pplTrigger.numResultsValue, + templateArgs[NUM_RESULTS_VALUE_FIELD] + ) + assertEquals( + "Template args field $CUSTOM_CONDITION_FIELD doesn't match", + pplTrigger.customCondition, + templateArgs[CUSTOM_CONDITION_FIELD] + ) + } +} diff --git a/core/src/main/kotlin/org/opensearch/alerting/core/util/XContentExtensions.kt b/core/src/main/kotlin/org/opensearch/alerting/core/util/XContentExtensions.kt new file mode 100644 index 000000000..9ca03ed6b --- /dev/null +++ b/core/src/main/kotlin/org/opensearch/alerting/core/util/XContentExtensions.kt @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.core.util + +import org.opensearch.core.xcontent.XContentBuilder +import java.time.Instant + +fun XContentBuilder.nonOptionalTimeField(name: String, instant: Instant): XContentBuilder { + return this.timeField(name, "${name}_in_millis", instant.toEpochMilli()) +} From e30b55ef9966e2b0ac535b41f376c5340faba8f8 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 23 Oct 2025 17:16:16 -0700 Subject: [PATCH 02/13] correcting readFrom in PPLTriggerRunResult Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt index a7a6076a4..4c24c148b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt @@ -8,8 +8,6 @@ package org.opensearch.alerting.modelv2 import org.opensearch.alerting.modelv2.TriggerV2RunResult.Companion.ERROR_FIELD import org.opensearch.alerting.modelv2.TriggerV2RunResult.Companion.NAME_FIELD import org.opensearch.alerting.modelv2.TriggerV2RunResult.Companion.TRIGGERED_FIELD -import org.opensearch.commons.alerting.model.QueryLevelTriggerRunResult -import org.opensearch.commons.alerting.model.TriggerRunResult import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput import org.opensearch.core.xcontent.ToXContent @@ -49,8 +47,8 @@ data class PPLTriggerRunResult( companion object { @JvmStatic @Throws(IOException::class) - fun readFrom(sin: StreamInput): TriggerRunResult { - return QueryLevelTriggerRunResult(sin) + fun readFrom(sin: StreamInput): TriggerV2RunResult { + return PPLTriggerRunResult(sin) } } } From 9c54e86f14e6f066fa73e13ac4e2279221a7ba10 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 24 Oct 2025 16:29:59 -0700 Subject: [PATCH 03/13] adding minutes to duration fields, changing name from PPLMonitor to PPLSQLMonitor Signed-off-by: Dennis Toepker --- .../opensearch/alerting/modelv2/MonitorV2.kt | 12 +++---- .../alerting/modelv2/MonitorV2RunResult.kt | 4 +-- .../{PPLMonitor.kt => PPLSQLMonitor.kt} | 36 ++++++++----------- ...RunResult.kt => PPLSQLMonitorRunResult.kt} | 8 ++--- .../{PPLTrigger.kt => PPLSQLTrigger.kt} | 18 +++++----- ...RunResult.kt => PPLSQLTriggerRunResult.kt} | 4 +-- .../opensearch/alerting/modelv2/TriggerV2.kt | 8 ++--- .../org/opensearch/alerting/TestHelpers.kt | 26 +++++++------- .../alerting/modelv2/MonitorV2Tests.kt | 6 ++-- .../alerting/modelv2/TriggerV2Tests.kt | 16 ++++----- 10 files changed, 66 insertions(+), 72 deletions(-) rename alerting/src/main/kotlin/org/opensearch/alerting/modelv2/{PPLMonitor.kt => PPLSQLMonitor.kt} (92%) rename alerting/src/main/kotlin/org/opensearch/alerting/modelv2/{PPLMonitorRunResult.kt => PPLSQLMonitorRunResult.kt} (88%) rename alerting/src/main/kotlin/org/opensearch/alerting/modelv2/{PPLTrigger.kt => PPLSQLTrigger.kt} (97%) rename alerting/src/main/kotlin/org/opensearch/alerting/modelv2/{PPLTriggerRunResult.kt => PPLSQLTriggerRunResult.kt} (95%) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt index 2fff45781..ab9e6db5b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -5,7 +5,7 @@ package org.opensearch.alerting.modelv2 -import org.opensearch.alerting.modelv2.PPLMonitor.Companion.PPL_MONITOR_TYPE +import org.opensearch.alerting.modelv2.PPLSQLMonitor.Companion.PPL_SQL_MONITOR_TYPE import org.opensearch.common.CheckedFunction import org.opensearch.commons.alerting.model.Schedule import org.opensearch.commons.alerting.model.ScheduledJob @@ -55,7 +55,7 @@ interface MonitorV2 : ScheduledJob { ): MonitorV2 enum class MonitorV2Type(val value: String) { - PPL_MONITOR(PPL_MONITOR_TYPE); + PPL_MONITOR(PPL_SQL_MONITOR_TYPE); override fun toString(): String { return value @@ -81,7 +81,7 @@ interface MonitorV2 : ScheduledJob { const val USER_FIELD = "user" const val TRIGGERS_FIELD = "triggers" const val SCHEMA_VERSION_FIELD = "schema_version" - const val LOOK_BACK_WINDOW_FIELD = "look_back_window" + const val LOOK_BACK_WINDOW_FIELD = "look_back_window_minutes" const val TIMESTAMP_FIELD = "timestamp_field" // default values @@ -122,20 +122,20 @@ interface MonitorV2 : ScheduledJob { XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner monitor object start return when (monitorType) { - MonitorV2Type.PPL_MONITOR -> PPLMonitor.parse(xcp) + MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor.parse(xcp) } } fun readFrom(sin: StreamInput): MonitorV2 { return when (val monitorType = sin.readEnum(MonitorV2Type::class.java)) { - MonitorV2Type.PPL_MONITOR -> PPLMonitor(sin) + MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor(sin) else -> throw IllegalStateException("Unexpected input \"$monitorType\" when reading MonitorV2") } } fun writeTo(out: StreamOutput, monitorV2: MonitorV2) { when (monitorV2) { - is PPLMonitor -> { + is PPLSQLMonitor -> { out.writeEnum(MonitorV2Type.PPL_MONITOR) monitorV2.writeTo(out) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt index cb36984ef..db56e3e1c 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt @@ -26,14 +26,14 @@ interface MonitorV2RunResult : Writeab fun readFrom(sin: StreamInput): MonitorV2RunResult { val monitorRunResultType = sin.readEnum(MonitorV2RunResultType::class.java) return when (monitorRunResultType) { - MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT -> PPLMonitorRunResult(sin) + MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT -> PPLSQLMonitorRunResult(sin) else -> throw IllegalStateException("Unexpected input [$monitorRunResultType] when reading MonitorV2RunResult") } } fun writeTo(out: StreamOutput, monitorV2RunResult: MonitorV2RunResult) { when (monitorV2RunResult) { - is PPLMonitorRunResult -> { + is PPLSQLMonitorRunResult -> { out.writeEnum(MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT) monitorV2RunResult.writeTo(out) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt similarity index 92% rename from alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index d5ac32837..a297d1fba 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -38,14 +38,8 @@ import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant -// TODO: eventually change this to be called PPLSQLMonitor. -// A PPL Monitor and SQL Monitor -// would have the exact same functionality, except the choice of language -// when calling PPL/SQL plugin's execute API would be different. -// we dont need 2 different monitor types for that, just a simple if check -// for query language at monitor execution time /** - * PPL (Piped Processing Language) Monitor for OpenSearch Alerting V2 + * PPL/SQL Monitor for OpenSearch Alerting V2 * * @property id Monitor ID. Defaults to [NO_ID]. * @property version Version number of the monitor. Defaults to [NO_VERSION]. @@ -58,9 +52,9 @@ import java.time.Instant * @property triggers List of [PPLTrigger]s associated with this monitor. * @property schemaVersion Version of the alerting-config index schema used when this Monitor was indexed. Defaults to [NO_SCHEMA_VERSION]. * @property queryLanguage The query language used. Defaults to [QueryLanguage.PPL]. - * @property query The PPL query string to be executed by this monitor. + * @property query The query string to be executed by this monitor. */ -data class PPLMonitor( +data class PPLSQLMonitor( override val id: String = NO_ID, override val version: Long = NO_VERSION, override val name: String, @@ -71,7 +65,7 @@ data class PPLMonitor( override val lastUpdateTime: Instant, override val enabledTime: Instant?, override val user: User?, - override val triggers: List, + override val triggers: List, override val schemaVersion: Int = IndexUtils.NO_SCHEMA_VERSION, val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported val query: String @@ -80,7 +74,7 @@ data class PPLMonitor( // specify scheduled job type override val type = MonitorV2.MONITOR_V2_TYPE - override fun fromDocument(id: String, version: Long): PPLMonitor = copy(id = id, version = version) + override fun fromDocument(id: String, version: Long): PPLSQLMonitor = copy(id = id, version = version) init { // SQL monitors are not yet supported @@ -136,7 +130,7 @@ data class PPLMonitor( } else { null }, - triggers = sin.readList(PPLTrigger.Companion::readFrom), + triggers = sin.readList(PPLSQLTrigger.Companion::readFrom), schemaVersion = sin.readInt(), queryLanguage = sin.readEnum(QueryLanguage::class.java), query = sin.readString() @@ -159,10 +153,10 @@ data class PPLMonitor( builder.startObject(MonitorV2.MONITOR_V2_TYPE) } - // wrap PPLMonitor in outer object named after its monitor type + // wrap PPLSQLMonitor in outer object named after its monitor type // required for MonitorV2 XContentParser to first encounter this, // read in monitor type, then delegate to correct parse() function - builder.startObject(PPL_MONITOR_TYPE) // monitor type start object + builder.startObject(PPL_SQL_MONITOR_TYPE) // monitor type start object builder.field(NAME_FIELD, name) builder.field(SCHEDULE_FIELD, schedule) @@ -247,7 +241,7 @@ data class PPLMonitor( schemaVersion: Int, lookBackWindow: Long?, timestampField: String? - ): PPLMonitor { + ): PPLSQLMonitor { return copy( id = id, version = version, @@ -274,7 +268,7 @@ data class PPLMonitor( companion object { // monitor type name - const val PPL_MONITOR_TYPE = "ppl_monitor" // TODO: eventually change to SQL_PPL_MONITOR_TYPE + const val PPL_SQL_MONITOR_TYPE = "ppl_sql_monitor" // query languages const val PPL_QUERY_LANGUAGE = "ppl" @@ -287,7 +281,7 @@ data class PPLMonitor( @JvmStatic @JvmOverloads @Throws(IOException::class) - fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): PPLMonitor { + fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): PPLSQLMonitor { var name: String? = null var enabled = true var schedule: Schedule? = null @@ -296,7 +290,7 @@ data class PPLMonitor( var lastUpdateTime: Instant? = null var enabledTime: Instant? = null var user: User? = null - val triggers: MutableList = mutableListOf() + val triggers: MutableList = mutableListOf() var schemaVersion = IndexUtils.NO_SCHEMA_VERSION var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL var query: String? = null @@ -327,7 +321,7 @@ data class PPLMonitor( xcp ) while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { - triggers.add(PPLTrigger.parseInner(xcp)) + triggers.add(PPLSQLTrigger.parseInner(xcp)) } } SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() @@ -364,8 +358,8 @@ data class PPLMonitor( requireNotNull(query) { "Query is null" } requireNotNull(lastUpdateTime) { "Last update time is null" } - /* return PPLMonitor */ - return PPLMonitor( + /* return PPLSQLMonitor */ + return PPLSQLMonitor( id, version, name, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitorRunResult.kt similarity index 88% rename from alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitorRunResult.kt index 5640c05fe..853ec58b8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLMonitorRunResult.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitorRunResult.kt @@ -14,19 +14,19 @@ import org.opensearch.core.xcontent.ToXContent import org.opensearch.core.xcontent.XContentBuilder import java.io.IOException -data class PPLMonitorRunResult( +data class PPLSQLMonitorRunResult( override val monitorName: String, override val error: Exception?, - override val triggerResults: Map, + override val triggerResults: Map, val pplQueryResults: Map> // key: trigger id, value: query results -) : MonitorV2RunResult { +) : MonitorV2RunResult { @Throws(IOException::class) @Suppress("UNCHECKED_CAST") constructor(sin: StreamInput) : this( sin.readString(), // monitorName sin.readException(), // error - sin.readMap() as Map, // triggerResults + sin.readMap() as Map, // triggerResults sin.readMap() as Map> // pplQueryResults ) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt similarity index 97% rename from alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt index 92caf46e3..6e907b6df 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTrigger.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt @@ -36,10 +36,10 @@ import java.io.IOException import java.time.Instant /** - * The PPL Trigger for PPL Monitors + * The PPL/SQL Trigger for PPL/SQL Monitors * * There are two types of PPLTrigger conditions: NUMBER_OF_RESULTS and CUSTOM - * NUMBER_OF_RESULTS: triggers based on whether the number of query results returned by the PPLMonitor + * NUMBER_OF_RESULTS: triggers based on whether the number of query results returned by the PPLSQLMonitor * query meets some threshold * CUSTOM: triggers based on a custom condition that user specifies (a single ppl eval statement) * @@ -66,7 +66,7 @@ import java.time.Instant * @property customCondition A custom condition expression. Required if using CUSTOM conditions, * required to be null otherwise. */ -data class PPLTrigger( +data class PPLSQLTrigger( override val id: String = UUIDs.base64UUID(), override val name: String, override val severity: Severity, @@ -248,7 +248,7 @@ data class PPLTrigger( companion object { // trigger wrapper object field name - const val PPL_TRIGGER_FIELD = "ppl_trigger" + const val PPL_SQL_TRIGGER_FIELD = "ppl_sql_trigger" // field names const val MODE_FIELD = "mode" @@ -259,13 +259,13 @@ data class PPLTrigger( val XCONTENT_REGISTRY = NamedXContentRegistry.Entry( TriggerV2::class.java, - ParseField(PPL_TRIGGER_FIELD), + ParseField(PPL_SQL_TRIGGER_FIELD), CheckedFunction { parseInner(it) } ) @JvmStatic @Throws(IOException::class) - fun parseInner(xcp: XContentParser): PPLTrigger { + fun parseInner(xcp: XContentParser): PPLSQLTrigger { var id = UUIDs.base64UUID() // assign a default triggerId if one is not specified var name: String? = null var severity: Severity? = null @@ -372,7 +372,7 @@ data class PPLTrigger( requireNotNull(conditionType) { "Trigger condition type must be included" } // 3. prepare and return PPLTrigger object - return PPLTrigger( + return PPLSQLTrigger( id, name, severity, @@ -390,8 +390,8 @@ data class PPLTrigger( @JvmStatic @Throws(IOException::class) - fun readFrom(sin: StreamInput): PPLTrigger { - return PPLTrigger(sin) + fun readFrom(sin: StreamInput): PPLSQLTrigger { + return PPLSQLTrigger(sin) } } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTriggerRunResult.kt similarity index 95% rename from alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTriggerRunResult.kt index 4c24c148b..70ea28a35 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLTriggerRunResult.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTriggerRunResult.kt @@ -14,7 +14,7 @@ import org.opensearch.core.xcontent.ToXContent import org.opensearch.core.xcontent.XContentBuilder import java.io.IOException -data class PPLTriggerRunResult( +data class PPLSQLTriggerRunResult( override var triggerName: String, override var triggered: Boolean, override var error: Exception?, @@ -48,7 +48,7 @@ data class PPLTriggerRunResult( @JvmStatic @Throws(IOException::class) fun readFrom(sin: StreamInput): TriggerV2RunResult { - return PPLTriggerRunResult(sin) + return PPLSQLTriggerRunResult(sin) } } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt index c54d3771e..14e726800 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/TriggerV2.kt @@ -5,7 +5,7 @@ package org.opensearch.alerting.modelv2 -import org.opensearch.alerting.modelv2.PPLTrigger.Companion.PPL_TRIGGER_FIELD +import org.opensearch.alerting.modelv2.PPLSQLTrigger.Companion.PPL_SQL_TRIGGER_FIELD import org.opensearch.commons.alerting.model.action.Action import org.opensearch.commons.notifications.model.BaseModel import java.time.Instant @@ -21,7 +21,7 @@ interface TriggerV2 : BaseModel { val actions: List enum class TriggerV2Type(val value: String) { - PPL_TRIGGER(PPL_TRIGGER_FIELD); + PPL_TRIGGER(PPL_SQL_TRIGGER_FIELD); override fun toString(): String { return value @@ -48,9 +48,9 @@ interface TriggerV2 : BaseModel { const val ID_FIELD = "id" const val NAME_FIELD = "name" const val SEVERITY_FIELD = "severity" - const val THROTTLE_FIELD = "throttle" + const val THROTTLE_FIELD = "throttle_minutes" const val LAST_TRIGGERED_FIELD = "last_triggered_time" - const val EXPIRE_FIELD = "expires" + const val EXPIRE_FIELD = "expires_minutes" const val ACTIONS_FIELD = "actions" // hard, nonadjustable limits diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index fefb64c70..4f3f6563d 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -13,12 +13,12 @@ import org.opensearch.alerting.model.destination.email.EmailAccount import org.opensearch.alerting.model.destination.email.EmailEntry import org.opensearch.alerting.model.destination.email.EmailGroup import org.opensearch.alerting.modelv2.AlertV2 -import org.opensearch.alerting.modelv2.PPLMonitor -import org.opensearch.alerting.modelv2.PPLMonitor.QueryLanguage -import org.opensearch.alerting.modelv2.PPLTrigger -import org.opensearch.alerting.modelv2.PPLTrigger.ConditionType -import org.opensearch.alerting.modelv2.PPLTrigger.NumResultsCondition -import org.opensearch.alerting.modelv2.PPLTrigger.TriggerMode +import org.opensearch.alerting.modelv2.PPLSQLMonitor +import org.opensearch.alerting.modelv2.PPLSQLMonitor.QueryLanguage +import org.opensearch.alerting.modelv2.PPLSQLTrigger +import org.opensearch.alerting.modelv2.PPLSQLTrigger.ConditionType +import org.opensearch.alerting.modelv2.PPLSQLTrigger.NumResultsCondition +import org.opensearch.alerting.modelv2.PPLSQLTrigger.TriggerMode import org.opensearch.alerting.modelv2.TriggerV2.Severity import org.opensearch.alerting.util.getBucketKeysHash import org.opensearch.client.Request @@ -316,12 +316,12 @@ fun randomPPLMonitor( timestampField: String? = lookBackWindow?.let { TIMESTAMP_FIELD }, lastUpdateTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), enabledTime: Instant? = if (enabled) Instant.now().truncatedTo(ChronoUnit.MILLIS) else null, - triggers: List = List(randomIntBetween(1, 5)) { randomPPLTrigger() }, + triggers: List = List(randomIntBetween(1, 5)) { randomPPLTrigger() }, user: User? = randomUser(), queryLanguage: QueryLanguage = QueryLanguage.PPL, query: String = "source = $TEST_INDEX_NAME | head 10" -): PPLMonitor { - return PPLMonitor( +): PPLSQLMonitor { + return PPLSQLMonitor( name = name, enabled = enabled, schedule = schedule, @@ -411,8 +411,8 @@ fun randomPPLTrigger( numResultsCondition: NumResultsCondition? = NumResultsCondition.entries.random(), numResultsValue: Long? = randomLongBetween(1L, 50L), customCondition: String? = null -): PPLTrigger { - return PPLTrigger( +): PPLSQLTrigger { + return PPLSQLTrigger( id = id, name = name, severity = severity, @@ -948,7 +948,7 @@ fun Map.objectMap(key: String): Map> { return this[key] as Map> } -fun assertPplMonitorsEqual(pplMonitor1: PPLMonitor, pplMonitor2: PPLMonitor) { +fun assertPplMonitorsEqual(pplMonitor1: PPLSQLMonitor, pplMonitor2: PPLSQLMonitor) { // note: Get and Search Monitor responses do not include User information by // design, so that check is skipped @@ -972,7 +972,7 @@ fun assertPplMonitorsEqual(pplMonitor1: PPLMonitor, pplMonitor2: PPLMonitor) { } } -fun assertPplTriggersEqual(pplTrigger1: PPLTrigger, pplTrigger2: PPLTrigger) { +fun assertPplTriggersEqual(pplTrigger1: PPLSQLTrigger, pplTrigger2: PPLSQLTrigger) { assertEquals( "Monitor trigger IDs not equal", pplTrigger1.id, diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt index 274a96899..a4c15a37a 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/MonitorV2Tests.kt @@ -14,7 +14,7 @@ import org.opensearch.alerting.modelv2.MonitorV2.Companion.LOOK_BACK_WINDOW_FIEL import org.opensearch.alerting.modelv2.MonitorV2.Companion.MONITOR_V2_MIN_LOOK_BACK_WINDOW import org.opensearch.alerting.modelv2.MonitorV2.Companion.NAME_FIELD import org.opensearch.alerting.modelv2.MonitorV2.Companion.SCHEDULE_FIELD -import org.opensearch.alerting.modelv2.PPLMonitor.Companion.QUERY_FIELD +import org.opensearch.alerting.modelv2.PPLSQLMonitor.Companion.QUERY_FIELD import org.opensearch.alerting.randomPPLMonitor import org.opensearch.alerting.randomPPLTrigger import org.opensearch.common.io.stream.BytesStreamOutput @@ -42,7 +42,7 @@ class MonitorV2Tests : OpenSearchTestCase() { } fun `test max triggers`() { - val tooManyTriggers = mutableListOf() + val tooManyTriggers = mutableListOf() for (i in 0..10) { // 11 times tooManyTriggers.add(randomPPLTrigger()) } @@ -108,7 +108,7 @@ class MonitorV2Tests : OpenSearchTestCase() { val out = BytesStreamOutput() pplMonitor.writeTo(out) val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) - val newPplMonitor = PPLMonitor(sin) + val newPplMonitor = PPLSQLMonitor(sin) assertPplMonitorsEqual(pplMonitor, newPplMonitor) } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt index 77fc19823..5b18629fe 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt @@ -7,13 +7,13 @@ package org.opensearch.alerting.modelv2 import org.opensearch.alerting.assertPplTriggersEqual import org.opensearch.alerting.modelv2.MonitorV2.Companion.ALERTING_V2_MAX_NAME_LENGTH -import org.opensearch.alerting.modelv2.PPLTrigger.Companion.CONDITION_TYPE_FIELD -import org.opensearch.alerting.modelv2.PPLTrigger.Companion.CUSTOM_CONDITION_FIELD -import org.opensearch.alerting.modelv2.PPLTrigger.Companion.MODE_FIELD -import org.opensearch.alerting.modelv2.PPLTrigger.Companion.NUM_RESULTS_CONDITION_FIELD -import org.opensearch.alerting.modelv2.PPLTrigger.Companion.NUM_RESULTS_VALUE_FIELD -import org.opensearch.alerting.modelv2.PPLTrigger.ConditionType -import org.opensearch.alerting.modelv2.PPLTrigger.NumResultsCondition +import org.opensearch.alerting.modelv2.PPLSQLTrigger.Companion.CONDITION_TYPE_FIELD +import org.opensearch.alerting.modelv2.PPLSQLTrigger.Companion.CUSTOM_CONDITION_FIELD +import org.opensearch.alerting.modelv2.PPLSQLTrigger.Companion.MODE_FIELD +import org.opensearch.alerting.modelv2.PPLSQLTrigger.Companion.NUM_RESULTS_CONDITION_FIELD +import org.opensearch.alerting.modelv2.PPLSQLTrigger.Companion.NUM_RESULTS_VALUE_FIELD +import org.opensearch.alerting.modelv2.PPLSQLTrigger.ConditionType +import org.opensearch.alerting.modelv2.PPLSQLTrigger.NumResultsCondition import org.opensearch.alerting.modelv2.TriggerV2.Companion.ACTIONS_FIELD import org.opensearch.alerting.modelv2.TriggerV2.Companion.EXPIRE_FIELD import org.opensearch.alerting.modelv2.TriggerV2.Companion.ID_FIELD @@ -174,7 +174,7 @@ class TriggerV2Tests : OpenSearchTestCase() { val out = BytesStreamOutput() pplTrigger.writeTo(out) val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) - val newPplTrigger = PPLTrigger(sin) + val newPplTrigger = PPLSQLTrigger(sin) assertPplTriggersEqual(pplTrigger, newPplTrigger) } From ff0458fe9aa89b7e8033cbc0a4051f974579e4cb Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 24 Oct 2025 16:42:00 -0700 Subject: [PATCH 04/13] adjusting comment positions Signed-off-by: Dennis Toepker --- .../kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt index ab9e6db5b..35dd27e89 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -111,7 +111,8 @@ interface MonitorV2 : ScheduledJob { xcp ) - XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) // monitor type field name + // monitor type field name + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) val monitorTypeText = xcp.currentName() val monitorType = MonitorV2Type.enumFromString(monitorTypeText) ?: throw IllegalStateException( @@ -119,7 +120,8 @@ interface MonitorV2 : ScheduledJob { "Please ensure monitor object is wrapped in an outer ppl_monitor object" ) - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner monitor object start + // inner monitor object start + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) return when (monitorType) { MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor.parse(xcp) From 7cee3c2dd41355b7d27cc3acdfe8a45bf73083fd Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 24 Oct 2025 22:00:56 -0700 Subject: [PATCH 05/13] adding description field to monitor Signed-off-by: Dennis Toepker --- .../opensearch/alerting/modelv2/MonitorV2.kt | 4 +++ .../alerting/modelv2/PPLSQLMonitor.kt | 25 ++++++++++++++----- .../org/opensearch/alerting/TestHelpers.kt | 2 ++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt index 35dd27e89..08314085b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -29,6 +29,7 @@ interface MonitorV2 : ScheduledJob { override val schedule: Schedule override val lastUpdateTime: Instant // required for scheduled job maintenance override val enabledTime: Instant? // required for scheduled job maintenance + val description: String? val user: User? val triggers: List val schemaVersion: Int // for updating monitors @@ -47,6 +48,7 @@ interface MonitorV2 : ScheduledJob { schedule: Schedule = this.schedule, lastUpdateTime: Instant = this.lastUpdateTime, enabledTime: Instant? = this.enabledTime, + description: String? = this.description, user: User? = this.user, // no support for overriding triggers in copy schemaVersion: Int = this.schemaVersion, @@ -78,6 +80,7 @@ interface MonitorV2 : ScheduledJob { const val SCHEDULE_FIELD = "schedule" const val LAST_UPDATE_TIME_FIELD = "last_update_time" const val ENABLED_TIME_FIELD = "enabled_time" + const val DESCRIPTION_FIELD = "description" const val USER_FIELD = "user" const val TRIGGERS_FIELD = "triggers" const val SCHEMA_VERSION_FIELD = "schema_version" @@ -93,6 +96,7 @@ interface MonitorV2 : ScheduledJob { const val MONITOR_V2_MIN_LOOK_BACK_WINDOW = 1L // 1 minute const val ALERTING_V2_MAX_NAME_LENGTH = 30 // max length of any name for monitors, triggers, notif actions, etc const val UUID_LENGTH = 20 // the length of a UUID generated by UUIDs.base64UUID() + const val DESCRIPTION_MAX_LENGTH = 2000 val XCONTENT_REGISTRY = NamedXContentRegistry.Entry( ScheduledJob::class.java, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index a297d1fba..641f7592b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -7,6 +7,8 @@ package org.opensearch.alerting.modelv2 import org.opensearch.alerting.core.util.nonOptionalTimeField import org.opensearch.alerting.modelv2.MonitorV2.Companion.ALERTING_V2_MAX_NAME_LENGTH +import org.opensearch.alerting.modelv2.MonitorV2.Companion.DESCRIPTION_FIELD +import org.opensearch.alerting.modelv2.MonitorV2.Companion.DESCRIPTION_MAX_LENGTH import org.opensearch.alerting.modelv2.MonitorV2.Companion.ENABLED_FIELD import org.opensearch.alerting.modelv2.MonitorV2.Companion.ENABLED_TIME_FIELD import org.opensearch.alerting.modelv2.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD @@ -64,6 +66,7 @@ data class PPLSQLMonitor( override val timestampField: String?, override val lastUpdateTime: Instant, override val enabledTime: Instant?, + override val description: String?, override val user: User?, override val triggers: List, override val schemaVersion: Int = IndexUtils.NO_SCHEMA_VERSION, @@ -78,7 +81,7 @@ data class PPLSQLMonitor( init { // SQL monitors are not yet supported - if (queryLanguage == QueryLanguage.SQL) { + if (this.queryLanguage == QueryLanguage.SQL) { throw IllegalArgumentException("SQL queries are not supported. Please use a PPL query.") } @@ -86,13 +89,13 @@ data class PPLSQLMonitor( "Monitor name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" } - if (lookBackWindow != null) { - requireNotNull(timestampField) { "If look back window is specified, timestamp field must not be null" } + if (this.lookBackWindow != null) { + requireNotNull(this.timestampField) { "If look back window is specified, timestamp field must not be null" } } else { - require(timestampField == null) { "If look back window is not specified, timestamp field must not be specified" } + require(this.timestampField == null) { "If look back window is not specified, timestamp field must not be specified" } } - require(triggers.isNotEmpty()) { "Monitor must include at least 1 trigger" } + require(this.triggers.isNotEmpty()) { "Monitor must include at least 1 trigger" } require(this.triggers.size <= MONITOR_V2_MAX_TRIGGERS) { "Monitors can only have $MONITOR_V2_MAX_TRIGGERS triggers" } lookBackWindow?.let { @@ -101,10 +104,12 @@ data class PPLSQLMonitor( } } + require(this.description?.length!! <= DESCRIPTION_MAX_LENGTH) { "Description must be under $DESCRIPTION_MAX_LENGTH characters" } + // for checking trigger ID uniqueness val triggerIds = mutableSetOf() this.triggers.forEach { trigger -> - require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique." } + require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique" } } if (this.enabled) { @@ -125,6 +130,7 @@ data class PPLSQLMonitor( timestampField = sin.readOptionalString(), lastUpdateTime = sin.readInstant(), enabledTime = sin.readOptionalInstant(), + description = sin.readOptionalString(), user = if (sin.readBoolean()) { User(sin) } else { @@ -165,6 +171,7 @@ data class PPLSQLMonitor( builder.field(ENABLED_FIELD, enabled) builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) + builder.field(DESCRIPTION_FIELD, description) builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) builder.field(SCHEMA_VERSION_FIELD, schemaVersion) builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) @@ -204,6 +211,7 @@ data class PPLSQLMonitor( out.writeOptionalString(timestampField) out.writeInstant(lastUpdateTime) out.writeOptionalInstant(enabledTime) + out.writeOptionalString(description) out.writeBoolean(user != null) user?.writeTo(out) @@ -237,6 +245,7 @@ data class PPLSQLMonitor( schedule: Schedule, lastUpdateTime: Instant, enabledTime: Instant?, + description: String?, user: User?, schemaVersion: Int, lookBackWindow: Long?, @@ -250,6 +259,7 @@ data class PPLSQLMonitor( schedule = schedule, lastUpdateTime = lastUpdateTime, enabledTime = enabledTime, + description = description, user = user, schemaVersion = schemaVersion, lookBackWindow = lookBackWindow, @@ -289,6 +299,7 @@ data class PPLSQLMonitor( var timestampField: String? = null var lastUpdateTime: Instant? = null var enabledTime: Instant? = null + var description: String? = null var user: User? = null val triggers: MutableList = mutableListOf() var schemaVersion = IndexUtils.NO_SCHEMA_VERSION @@ -313,6 +324,7 @@ data class PPLSQLMonitor( TIMESTAMP_FIELD -> timestampField = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) null else xcp.text() LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() ENABLED_TIME_FIELD -> enabledTime = xcp.instant() + DESCRIPTION_FIELD -> description = xcp.text() USER_FIELD -> user = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) null else User.parse(xcp) TRIGGERS_FIELD -> { XContentParserUtils.ensureExpectedToken( @@ -369,6 +381,7 @@ data class PPLSQLMonitor( timestampField, lastUpdateTime, enabledTime, + description, user, triggers, schemaVersion, diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index 4f3f6563d..2806e2799 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -316,6 +316,7 @@ fun randomPPLMonitor( timestampField: String? = lookBackWindow?.let { TIMESTAMP_FIELD }, lastUpdateTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), enabledTime: Instant? = if (enabled) Instant.now().truncatedTo(ChronoUnit.MILLIS) else null, + description: String? = "some description", triggers: List = List(randomIntBetween(1, 5)) { randomPPLTrigger() }, user: User? = randomUser(), queryLanguage: QueryLanguage = QueryLanguage.PPL, @@ -329,6 +330,7 @@ fun randomPPLMonitor( timestampField = timestampField, lastUpdateTime = lastUpdateTime, enabledTime = enabledTime, + description = description, triggers = triggers, user = user, queryLanguage = queryLanguage, From 95b5fe342654e87746e8b69090343a59cd19a643 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 24 Oct 2025 23:22:10 -0700 Subject: [PATCH 06/13] adding description javadoc Signed-off-by: Dennis Toepker --- .../main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index 641f7592b..934a13b50 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -51,6 +51,7 @@ import java.time.Instant * @property lookBackWindow How far back each Monitor execution's query should look back when searching data. * @property lastUpdateTime Timestamp of the last update to this monitor. * @property enabledTime Timestamp when the monitor was last enabled. Null if never enabled. + * @property description Optional Monitor description. * @property triggers List of [PPLTrigger]s associated with this monitor. * @property schemaVersion Version of the alerting-config index schema used when this Monitor was indexed. Defaults to [NO_SCHEMA_VERSION]. * @property queryLanguage The query language used. Defaults to [QueryLanguage.PPL]. From 34a3e77801e801be57468dd9b6045045a4b995d6 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sun, 26 Oct 2025 15:25:07 -0700 Subject: [PATCH 07/13] various refactors Signed-off-by: Dennis Toepker --- .../opensearch/alerting/modelv2/AlertV2.kt | 12 +------- .../opensearch/alerting/modelv2/MonitorV2.kt | 9 +++--- .../alerting/modelv2/PPLSQLMonitor.kt | 24 +++++++++++---- .../org/opensearch/alerting/TestHelpers.kt | 29 ++----------------- .../alerting/modelv2/AlertV2Tests.kt | 6 ---- 5 files changed, 27 insertions(+), 53 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt index b42f2c2b2..7bcd76cc7 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt @@ -35,7 +35,6 @@ import java.time.Instant * @property triggerName Name of the trigger in the Monitor that generated this alert. * @property queryResults Results from the Monitor's query that caused the Trigger to fire. * @property triggeredTime Timestamp for when the Alert was generated. - * @property expirationTime Timestamp for when the Alert should be expired. * @property errorMessage Optional error message if there were issues during Trigger execution. * Null indicates no errors occurred. * @property severity Severity level of the alert (e.g., "HIGH", "MEDIUM", "LOW"). @@ -47,7 +46,7 @@ import java.time.Instant * Lifecycle: * 1. AlertV2 is generated when a TriggerV2's condition is met. The TriggerV2 fires and forgets the AlertV2. * 2. AlertV2 is stored in the alerts index. AlertV2s are stateless. (e.g. they are never ACTIVE or COMPLETED) - * 3. AlertV2 is soft deleted at [expirationTime], and archived in an alert history index + * 3. AlertV2 is soft deleted after its expire duration (determined by its trigger), and archived in an alert history index * 4. Based on the alert v2 history retention period, the AlertV2 is permanently deleted */ data class AlertV2( @@ -63,7 +62,6 @@ data class AlertV2( val query: String, val queryResults: Map, val triggeredTime: Instant, - val expirationTime: Instant, val errorMessage: String? = null, val severity: Severity, val executionId: String? = null @@ -86,7 +84,6 @@ data class AlertV2( query = sin.readString(), queryResults = sin.readMap(), triggeredTime = sin.readInstant(), - expirationTime = sin.readInstant(), errorMessage = sin.readOptionalString(), severity = sin.readEnum(Severity::class.java), executionId = sin.readOptionalString() @@ -107,7 +104,6 @@ data class AlertV2( out.writeString(query) out.writeMap(queryResults) out.writeInstant(triggeredTime) - out.writeInstant(expirationTime) out.writeOptionalString(errorMessage) out.writeEnum(severity) out.writeOptionalString(executionId) @@ -137,7 +133,6 @@ data class AlertV2( .field(ERROR_MESSAGE_FIELD, errorMessage) .field(SEVERITY_FIELD, severity.value) .nonOptionalTimeField(TRIGGERED_TIME_FIELD, triggeredTime) - .nonOptionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) if (withUser) { builder.optionalUserField(MONITOR_V2_USER_FIELD, monitorUser) @@ -154,7 +149,6 @@ data class AlertV2( ALERT_V2_VERSION_FIELD to version, ERROR_MESSAGE_FIELD to errorMessage, EXECUTION_ID_FIELD to executionId, - EXPIRATION_TIME_FIELD to expirationTime.toEpochMilli(), SEVERITY_FIELD to severity.value ) } @@ -169,7 +163,6 @@ data class AlertV2( const val TRIGGER_V2_ID_FIELD = "trigger_v2_id" const val TRIGGER_V2_NAME_FIELD = "trigger_v2_name" const val TRIGGERED_TIME_FIELD = "triggered_time" - const val EXPIRATION_TIME_FIELD = "expiration_time" const val QUERY_FIELD = "query" const val QUERY_RESULTS_FIELD = "query_results" const val ERROR_MESSAGE_FIELD = "error_message" @@ -196,7 +189,6 @@ data class AlertV2( var queryResults: Map = mapOf() lateinit var severity: Severity var triggeredTime: Instant? = null - var expirationTime: Instant? = null var errorMessage: String? = null var executionId: String? = null @@ -221,7 +213,6 @@ data class AlertV2( QUERY_FIELD -> query = xcp.text() QUERY_RESULTS_FIELD -> queryResults = xcp.map() TRIGGERED_TIME_FIELD -> triggeredTime = xcp.instant() - EXPIRATION_TIME_FIELD -> expirationTime = xcp.instant() ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull() EXECUTION_ID_FIELD -> executionId = xcp.textOrNull() TriggerV2.SEVERITY_FIELD -> { @@ -249,7 +240,6 @@ data class AlertV2( query = requireNotNull(query), queryResults = requireNotNull(queryResults), triggeredTime = requireNotNull(triggeredTime), - expirationTime = requireNotNull(expirationTime), errorMessage = errorMessage, severity = severity, executionId = executionId diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt index 08314085b..f394db73d 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -50,7 +50,8 @@ interface MonitorV2 : ScheduledJob { enabledTime: Instant? = this.enabledTime, description: String? = this.description, user: User? = this.user, - // no support for overriding triggers in copy + // no support for overriding triggers in interface-level makeCopy(), + // triggers can be copied at instance-level data class copy() schemaVersion: Int = this.schemaVersion, lookBackWindow: Long? = this.lookBackWindow, timestampField: String? = this.timestampField @@ -106,7 +107,7 @@ interface MonitorV2 : ScheduledJob { @JvmStatic @Throws(IOException::class) - fun parse(xcp: XContentParser): MonitorV2 { + fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): MonitorV2 { /* parse outer object for monitorV2 type, then delegate to correct monitorV2 parser */ XContentParserUtils.ensureExpectedToken( // outer monitor object start @@ -121,14 +122,14 @@ interface MonitorV2 : ScheduledJob { val monitorType = MonitorV2Type.enumFromString(monitorTypeText) ?: throw IllegalStateException( "when parsing MonitorV2, received invalid monitor type: $monitorTypeText. " + - "Please ensure monitor object is wrapped in an outer ppl_monitor object" + "Please ensure monitor object is wrapped in an outer ppl_sql_monitor object" ) // inner monitor object start XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) return when (monitorType) { - MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor.parse(xcp) + MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor.parse(xcp, id, version) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index 934a13b50..23f5add2f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -105,7 +105,9 @@ data class PPLSQLMonitor( } } - require(this.description?.length!! <= DESCRIPTION_MAX_LENGTH) { "Description must be under $DESCRIPTION_MAX_LENGTH characters" } + this.description?.let { + require(this.description.length <= DESCRIPTION_MAX_LENGTH) { "Description must be under $DESCRIPTION_MAX_LENGTH characters" } + } // for checking trigger ID uniqueness val triggerIds = mutableSetOf() @@ -322,11 +324,23 @@ data class PPLSQLMonitor( lookBackWindow = xcp.longValue() } } - TIMESTAMP_FIELD -> timestampField = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) null else xcp.text() + TIMESTAMP_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + timestampField = xcp.text() + } + } LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() ENABLED_TIME_FIELD -> enabledTime = xcp.instant() - DESCRIPTION_FIELD -> description = xcp.text() - USER_FIELD -> user = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) null else User.parse(xcp) + DESCRIPTION_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + description = xcp.text() + } + } + USER_FIELD -> { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { + user = User.parse(xcp) + } + } TRIGGERS_FIELD -> { XContentParserUtils.ensureExpectedToken( XContentParser.Token.START_ARRAY, @@ -350,7 +364,7 @@ data class PPLSQLMonitor( queryLanguage = enumMatchResult } QUERY_FIELD -> query = xcp.text() - else -> throw IllegalArgumentException("Unexpected field when parsing PPL Monitor: $fieldName") + else -> throw IllegalArgumentException("Unexpected field when parsing PPL/SQL Monitor: $fieldName") } } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index 2806e2799..6f8c05d09 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -312,11 +312,11 @@ fun randomPPLMonitor( name: String = OpenSearchRestTestCase.randomAlphaOfLength(10), enabled: Boolean = randomBoolean(), schedule: Schedule = IntervalSchedule(interval = 5, unit = ChronoUnit.MINUTES), - lookBackWindow: Long? = randomLongBetween(1, 100), + lookBackWindow: Long? = randomLongBetween(10, 100), timestampField: String? = lookBackWindow?.let { TIMESTAMP_FIELD }, lastUpdateTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), enabledTime: Instant? = if (enabled) Instant.now().truncatedTo(ChronoUnit.MILLIS) else null, - description: String? = "some description", + description: String? = if (randomBoolean()) "some description" else null, triggers: List = List(randomIntBetween(1, 5)) { randomPPLTrigger() }, user: User? = randomUser(), queryLanguage: QueryLanguage = QueryLanguage.PPL, @@ -555,24 +555,6 @@ fun randomAlert(monitor: Monitor = randomQueryLevelMonitor()): Alert { ) } -/* -val id: String = NO_ID, -val version: Long = NO_VERSION, -val schemaVersion: Int = NO_SCHEMA_VERSION, -val monitorId: String, -val monitorName: String, -val monitorVersion: Long, -val monitorUser: User?, -val triggerId: String, -val triggerName: String, -val query: String, -val queryResults: Map, -val triggeredTime: Instant, -val expirationTime: Instant, -val errorMessage: String? = null, -val severity: Severity, -val executionId: String? = null - */ fun randomAlertV2( id: String = UUIDs.base64UUID(), version: Long = randomLongBetween(1, 10), @@ -586,7 +568,6 @@ fun randomAlertV2( query: String = "source = $TEST_INDEX_NAME | head 10", queryResults: Map = mapOf(), triggeredTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), - expirationTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), errorMessage: String? = "sample error message", severity: Severity = Severity.entries.random(), executionId: String? = UUIDs.base64UUID() @@ -604,7 +585,6 @@ fun randomAlertV2( query = query, queryResults = queryResults, triggeredTime = triggeredTime, - expirationTime = expirationTime, errorMessage = errorMessage, severity = severity, executionId = executionId, @@ -1091,11 +1071,6 @@ fun assertAlertV2sEqual(alert1: AlertV2, alert2: AlertV2) { alert1.triggeredTime, alert2.triggeredTime ) - assertEquals( - "AlertV2 expiration times are not equal", - alert1.expirationTime, - alert2.expirationTime - ) assertEquals( "AlertV2 error messages are not equal", alert1.errorMessage, diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt index 6b40801e4..fade152bc 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/AlertV2Tests.kt @@ -10,7 +10,6 @@ import org.opensearch.alerting.modelv2.AlertV2.Companion.ALERT_V2_ID_FIELD import org.opensearch.alerting.modelv2.AlertV2.Companion.ALERT_V2_VERSION_FIELD import org.opensearch.alerting.modelv2.AlertV2.Companion.ERROR_MESSAGE_FIELD import org.opensearch.alerting.modelv2.AlertV2.Companion.EXECUTION_ID_FIELD -import org.opensearch.alerting.modelv2.AlertV2.Companion.EXPIRATION_TIME_FIELD import org.opensearch.alerting.modelv2.AlertV2.Companion.SEVERITY_FIELD import org.opensearch.alerting.randomAlertV2 import org.opensearch.common.io.stream.BytesStreamOutput @@ -51,11 +50,6 @@ class AlertV2Tests : OpenSearchTestCase() { alertV2.executionId, templateArgs[EXECUTION_ID_FIELD] ) - assertEquals( - "Template args field $EXPIRATION_TIME_FIELD doesn't match", - alertV2.expirationTime.toEpochMilli(), - templateArgs[EXPIRATION_TIME_FIELD] - ) assertEquals( "Template args field $SEVERITY_FIELD doesn't match", alertV2.severity.value, From 79e96999b85c5d6ac0ec2daacd0be269ca2ef591 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sun, 26 Oct 2025 23:08:19 -0700 Subject: [PATCH 08/13] making alert initiate its own parsing pointer Signed-off-by: Dennis Toepker --- .../src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt index 7bcd76cc7..7f60201c6 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt @@ -192,7 +192,7 @@ data class AlertV2( var errorMessage: String? = null var executionId: String? = null - ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { val fieldName = xcp.currentName() xcp.nextToken() From 2827aeadb895981f4034bfc3e30ca22f4f9e64c1 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 Oct 2025 09:45:39 -0700 Subject: [PATCH 09/13] adding number of results value validations Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/modelv2/PPLSQLTrigger.kt | 4 ++++ .../org/opensearch/alerting/modelv2/TriggerV2Tests.kt | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt index 6e907b6df..6b6253ca6 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt @@ -144,6 +144,10 @@ data class PPLSQLTrigger( } } } + + if (conditionType == ConditionType.NUMBER_OF_RESULTS) { + require(this.numResultsValue!! >= 0L) { "Number of results to check for cannot be negative" } + } } @Throws(IOException::class) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt index 5b18629fe..430572254 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/modelv2/TriggerV2Tests.kt @@ -61,6 +61,17 @@ class TriggerV2Tests : OpenSearchTestCase() { } catch (_: IllegalArgumentException) {} } + fun `test number of results trigger with negative number of results value`() { + try { + randomPPLTrigger( + conditionType = ConditionType.NUMBER_OF_RESULTS, + numResultsValue = -1L, + numResultsCondition = NumResultsCondition.GREATER_THAN + ) + fail("Number of results trigger with negative number of results value should be rejected.") + } catch (_: IllegalArgumentException) {} + } + fun `test trigger action name too long`() { var actionName = "" for (i in 0 until ALERTING_V2_MAX_NAME_LENGTH + 1) { From 92491f2033335d903f761c283a0dd7c6673c9110 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 Oct 2025 10:04:29 -0700 Subject: [PATCH 10/13] adding full stops to error messages Signed-off-by: Dennis Toepker --- .../alerting/modelv2/PPLSQLMonitor.kt | 14 +++--- .../alerting/modelv2/PPLSQLTrigger.kt | 46 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index 23f5add2f..99b9abb05 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -87,32 +87,32 @@ data class PPLSQLMonitor( } require(this.name.length <= ALERTING_V2_MAX_NAME_LENGTH) { - "Monitor name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" + "Monitor name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH." } if (this.lookBackWindow != null) { - requireNotNull(this.timestampField) { "If look back window is specified, timestamp field must not be null" } + requireNotNull(this.timestampField) { "If look back window is specified, timestamp field must not be null." } } else { - require(this.timestampField == null) { "If look back window is not specified, timestamp field must not be specified" } + require(this.timestampField == null) { "If look back window is not specified, timestamp field must not be specified." } } require(this.triggers.isNotEmpty()) { "Monitor must include at least 1 trigger" } - require(this.triggers.size <= MONITOR_V2_MAX_TRIGGERS) { "Monitors can only have $MONITOR_V2_MAX_TRIGGERS triggers" } + require(this.triggers.size <= MONITOR_V2_MAX_TRIGGERS) { "Monitors can only have $MONITOR_V2_MAX_TRIGGERS triggers." } lookBackWindow?.let { require(this.lookBackWindow >= MONITOR_V2_MIN_LOOK_BACK_WINDOW) { - "Monitors look back windows must be at least $MONITOR_V2_MIN_LOOK_BACK_WINDOW minute" + "Monitors look back windows must be at least $MONITOR_V2_MIN_LOOK_BACK_WINDOW minute." } } this.description?.let { - require(this.description.length <= DESCRIPTION_MAX_LENGTH) { "Description must be under $DESCRIPTION_MAX_LENGTH characters" } + require(this.description.length <= DESCRIPTION_MAX_LENGTH) { "Description must be under $DESCRIPTION_MAX_LENGTH characters." } } // for checking trigger ID uniqueness val triggerIds = mutableSetOf() this.triggers.forEach { trigger -> - require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique" } + require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique." } } if (this.enabled) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt index 6b6253ca6..c5c058310 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLTrigger.kt @@ -82,71 +82,71 @@ data class PPLSQLTrigger( ) : TriggerV2 { init { - requireNotNull(this.name) { "Trigger name must be included" } - requireNotNull(this.severity) { "Trigger severity must be included" } - requireNotNull(this.mode) { "Trigger mode must be included" } - requireNotNull(this.conditionType) { "Trigger condition type must be included" } + requireNotNull(this.name) { "Trigger name must be included." } + requireNotNull(this.severity) { "Trigger severity must be included." } + requireNotNull(this.mode) { "Trigger mode must be included." } + requireNotNull(this.conditionType) { "Trigger condition type must be included." } require(this.id.length <= UUID_LENGTH) { - "Trigger ID too long, length must be less than $UUID_LENGTH" + "Trigger ID too long, length must be less than $UUID_LENGTH." } require(this.name.length <= ALERTING_V2_MAX_NAME_LENGTH) { - "Trigger name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" + "Trigger name too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH." } require(this.expireDuration >= MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES) { - "expire duration cannot be less than $MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES, was $expireDuration" + "expire duration cannot be less than $MONITOR_V2_MIN_EXPIRE_DURATION_MINUTES, was $expireDuration." } this.throttleDuration?.let { require(it >= MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES) { - "Throttle duration cannot be less than $MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES, was $throttleDuration" + "Throttle duration cannot be less than $MONITOR_V2_MIN_THROTTLE_DURATION_MINUTES, was $throttleDuration." } } this.actions.forEach { require(it.name.length <= ALERTING_V2_MAX_NAME_LENGTH) { - "Name of action with ID ${it.id} too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH" + "Name of action with ID ${it.id} too long, length must be less than $ALERTING_V2_MAX_NAME_LENGTH." } require(it.destinationId.length <= NOTIFICATIONS_ID_MAX_LENGTH) { - "Channel ID of action with ID ${it.id} too long, length must be less than $NOTIFICATIONS_ID_MAX_LENGTH" + "Channel ID of action with ID ${it.id} too long, length must be less than $NOTIFICATIONS_ID_MAX_LENGTH." } } when (this.conditionType) { ConditionType.NUMBER_OF_RESULTS -> { requireNotNull(this.numResultsCondition) { - "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}," + - "$NUM_RESULTS_CONDITION_FIELD must be included" + "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, " + + "$NUM_RESULTS_CONDITION_FIELD must be included." } requireNotNull(this.numResultsValue) { - "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}," + - "$NUM_RESULTS_VALUE_FIELD must be included" + "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, " + + "$NUM_RESULTS_VALUE_FIELD must be included." } require(this.customCondition == null) { - "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}," + - "$CUSTOM_CONDITION_FIELD must not be included" + "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, " + + "$CUSTOM_CONDITION_FIELD must not be included." } } ConditionType.CUSTOM -> { requireNotNull(this.customCondition) { - "if trigger condition is of type ${ConditionType.CUSTOM.value}," + - "$CUSTOM_CONDITION_FIELD must be included" + "if trigger condition is of type ${ConditionType.CUSTOM.value}, " + + "$CUSTOM_CONDITION_FIELD must be included." } require(this.numResultsCondition == null) { - "if trigger condition is of type ${ConditionType.CUSTOM.value}," + - "$NUM_RESULTS_CONDITION_FIELD must not be included" + "if trigger condition is of type ${ConditionType.CUSTOM.value}, " + + "$NUM_RESULTS_CONDITION_FIELD must not be included." } require(this.numResultsValue == null) { - "if trigger condition is of type ${ConditionType.CUSTOM.value}," + - "$NUM_RESULTS_VALUE_FIELD must not be included" + "if trigger condition is of type ${ConditionType.CUSTOM.value}, " + + "$NUM_RESULTS_VALUE_FIELD must not be included." } } } if (conditionType == ConditionType.NUMBER_OF_RESULTS) { - require(this.numResultsValue!! >= 0L) { "Number of results to check for cannot be negative" } + require(this.numResultsValue!! >= 0L) { "Number of results to check for cannot be negative." } } } From 5b7b9a27039b4b20bc104979eafc6c42da13c76c Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 Oct 2025 10:06:43 -0700 Subject: [PATCH 11/13] adding another missing full stop Signed-off-by: Dennis Toepker --- .../kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index 99b9abb05..5bdff2666 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -96,7 +96,7 @@ data class PPLSQLMonitor( require(this.timestampField == null) { "If look back window is not specified, timestamp field must not be specified." } } - require(this.triggers.isNotEmpty()) { "Monitor must include at least 1 trigger" } + require(this.triggers.isNotEmpty()) { "Monitor must include at least 1 trigger." } require(this.triggers.size <= MONITOR_V2_MAX_TRIGGERS) { "Monitors can only have $MONITOR_V2_MAX_TRIGGERS triggers." } lookBackWindow?.let { From f968289f5545347c1a83cb3d5e90e3338c24be9b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 Oct 2025 11:17:28 -0700 Subject: [PATCH 12/13] renaming customer exposed field name back to ppl_monitor Signed-off-by: Dennis Toepker --- .../main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt | 2 +- .../kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt index f394db73d..5065d5f11 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -122,7 +122,7 @@ interface MonitorV2 : ScheduledJob { val monitorType = MonitorV2Type.enumFromString(monitorTypeText) ?: throw IllegalStateException( "when parsing MonitorV2, received invalid monitor type: $monitorTypeText. " + - "Please ensure monitor object is wrapped in an outer ppl_sql_monitor object" + "Please ensure monitor object is wrapped in an outer $PPL_SQL_MONITOR_TYPE object" ) // inner monitor object start diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt index 5bdff2666..edf8e7c4d 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/PPLSQLMonitor.kt @@ -281,7 +281,7 @@ data class PPLSQLMonitor( companion object { // monitor type name - const val PPL_SQL_MONITOR_TYPE = "ppl_sql_monitor" + const val PPL_SQL_MONITOR_TYPE = "ppl_monitor" // query languages const val PPL_QUERY_LANGUAGE = "ppl" From 8eb0651e3c85540d2da6e122f449d5709c1f14df Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 Oct 2025 11:37:44 -0700 Subject: [PATCH 13/13] renaming PPL to PPL_SQL in certain places Signed-off-by: Dennis Toepker --- .../kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt | 8 ++++---- .../org/opensearch/alerting/modelv2/MonitorV2RunResult.kt | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt index 5065d5f11..69485b4c9 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2.kt @@ -58,7 +58,7 @@ interface MonitorV2 : ScheduledJob { ): MonitorV2 enum class MonitorV2Type(val value: String) { - PPL_MONITOR(PPL_SQL_MONITOR_TYPE); + PPL_SQL_MONITOR(PPL_SQL_MONITOR_TYPE); override fun toString(): String { return value @@ -129,13 +129,13 @@ interface MonitorV2 : ScheduledJob { XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) return when (monitorType) { - MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor.parse(xcp, id, version) + MonitorV2Type.PPL_SQL_MONITOR -> PPLSQLMonitor.parse(xcp, id, version) } } fun readFrom(sin: StreamInput): MonitorV2 { return when (val monitorType = sin.readEnum(MonitorV2Type::class.java)) { - MonitorV2Type.PPL_MONITOR -> PPLSQLMonitor(sin) + MonitorV2Type.PPL_SQL_MONITOR -> PPLSQLMonitor(sin) else -> throw IllegalStateException("Unexpected input \"$monitorType\" when reading MonitorV2") } } @@ -143,7 +143,7 @@ interface MonitorV2 : ScheduledJob { fun writeTo(out: StreamOutput, monitorV2: MonitorV2) { when (monitorV2) { is PPLSQLMonitor -> { - out.writeEnum(MonitorV2Type.PPL_MONITOR) + out.writeEnum(MonitorV2Type.PPL_SQL_MONITOR) monitorV2.writeTo(out) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt index db56e3e1c..f6707eb78 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/modelv2/MonitorV2RunResult.kt @@ -16,7 +16,7 @@ interface MonitorV2RunResult : Writeab val triggerResults: Map enum class MonitorV2RunResultType() { - PPL_MONITOR_RUN_RESULT; + PPL_SQL_MONITOR_RUN_RESULT; } companion object { @@ -26,7 +26,7 @@ interface MonitorV2RunResult : Writeab fun readFrom(sin: StreamInput): MonitorV2RunResult { val monitorRunResultType = sin.readEnum(MonitorV2RunResultType::class.java) return when (monitorRunResultType) { - MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT -> PPLSQLMonitorRunResult(sin) + MonitorV2RunResultType.PPL_SQL_MONITOR_RUN_RESULT -> PPLSQLMonitorRunResult(sin) else -> throw IllegalStateException("Unexpected input [$monitorRunResultType] when reading MonitorV2RunResult") } } @@ -34,7 +34,7 @@ interface MonitorV2RunResult : Writeab fun writeTo(out: StreamOutput, monitorV2RunResult: MonitorV2RunResult) { when (monitorV2RunResult) { is PPLSQLMonitorRunResult -> { - out.writeEnum(MonitorV2RunResultType.PPL_MONITOR_RUN_RESULT) + out.writeEnum(MonitorV2RunResultType.PPL_SQL_MONITOR_RUN_RESULT) monitorV2RunResult.writeTo(out) } }