From 046f74130259c745df6509c6581bc462170185fd Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 6 Aug 2025 14:45:15 -0700 Subject: [PATCH 01/30] initial alertingv2 models --- .../alerting/action/AlertingActions.kt | 8 + .../alerting/action/IndexMonitorV2Request.kt | 63 +++++ .../alerting/action/IndexMonitorV2Response.kt | 69 +++++ .../commons/alerting/model/MonitorV2.kt | 108 ++++++++ .../alerting/model/MonitorV2RunResult.kt | 69 +++++ .../commons/alerting/model/PPLMonitor.kt | 239 ++++++++++++++++++ .../alerting/model/PPLMonitorRunResult.kt | 73 ++++++ .../commons/alerting/model/PPLTrigger.kt | 220 ++++++++++++++++ .../alerting/model/PPLTriggerRunResult.kt | 73 ++++++ .../commons/alerting/model/TriggerV2.kt | 79 ++++++ .../alerting/model/TriggerV2RunResult.kt | 28 ++ .../commons/alerting/util/IndexUtils.kt | 5 + 12 files changed, 1034 insertions(+) create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt index fcf98261..302d35d3 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt @@ -8,6 +8,7 @@ import org.opensearch.action.ActionType import org.opensearch.action.search.SearchResponse object AlertingActions { + // Alerting V1 const val INDEX_MONITOR_ACTION_NAME = "cluster:admin/opendistro/alerting/monitor/write" const val INDEX_WORKFLOW_ACTION_NAME = "cluster:admin/opensearch/alerting/workflow/write" const val GET_ALERTS_ACTION_NAME = "cluster:admin/opendistro/alerting/alerts/get" @@ -25,6 +26,9 @@ object AlertingActions { const val SEARCH_COMMENTS_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/search" const val DELETE_COMMENT_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/delete" + // Alerting V2 + const val INDEX_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/write" + @JvmField val INDEX_MONITOR_ACTION_TYPE = ActionType(INDEX_MONITOR_ACTION_NAME, ::IndexMonitorResponse) @@ -88,4 +92,8 @@ object AlertingActions { @JvmField val DELETE_COMMENT_ACTION_TYPE = ActionType(DELETE_COMMENT_ACTION_NAME, ::DeleteCommentResponse) + + @JvmField + val INDEX_MONITOR_V2_ACTION_TYPE = + ActionType(INDEX_MONITOR_V2_ACTION_NAME, ::IndexMonitorV2Response) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt new file mode 100644 index 00000000..4617aec3 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt @@ -0,0 +1,63 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.action.support.WriteRequest +import org.opensearch.commons.alerting.model.MonitorV2 +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput + +class IndexMonitorV2Request : ActionRequest { +// val monitorId: String + val seqNo: Long + val primaryTerm: Long + val refreshPolicy: WriteRequest.RefreshPolicy +// val method: RestRequest.Method + var monitorV2: MonitorV2 +// val rbacRoles: List? + + constructor( +// monitorId: String, + seqNo: Long, + primaryTerm: Long, + refreshPolicy: WriteRequest.RefreshPolicy, +// method: RestRequest.Method, + monitorV2: MonitorV2, +// rbacRoles: List? = null + ) : super() { +// this.monitorId = monitorId + this.seqNo = seqNo + this.primaryTerm = primaryTerm + this.refreshPolicy = refreshPolicy +// this.method = method + this.monitorV2 = monitorV2 +// this.rbacRoles = rbacRoles + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( +// monitorId = sin.readString(), + seqNo = sin.readLong(), + primaryTerm = sin.readLong(), + refreshPolicy = WriteRequest.RefreshPolicy.readFrom(sin), +// method = sin.readEnum(RestRequest.Method::class.java), + monitorV2 = MonitorV2.readFrom(sin), +// rbacRoles = sin.readOptionalStringList() + ) + + override fun validate(): ActionRequestValidationException? { + return null + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { +// out.writeString(monitorId) + out.writeLong(seqNo) + out.writeLong(primaryTerm) + refreshPolicy.writeTo(out) +// out.writeEnum(method) + MonitorV2.writeTo(out, monitorV2) +// out.writeOptionalStringCollection(rbacRoles) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt new file mode 100644 index 00000000..9d481325 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt @@ -0,0 +1,69 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.commons.alerting.model.Monitor +import org.opensearch.commons.alerting.model.MonitorV2 +import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID +import org.opensearch.commons.alerting.util.IndexUtils.Companion._PRIMARY_TERM +import org.opensearch.commons.alerting.util.IndexUtils.Companion._SEQ_NO +import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION +import org.opensearch.commons.notifications.action.BaseResponse +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 + +class IndexMonitorV2Response : BaseResponse { + var id: String + var version: Long + var seqNo: Long + var primaryTerm: Long + var monitorV2: MonitorV2 + + constructor( + id: String, + version: Long, + seqNo: Long, + primaryTerm: Long, + monitorV2: MonitorV2 + ) : super() { + this.id = id + this.version = version + this.seqNo = seqNo + this.primaryTerm = primaryTerm + this.monitorV2 = monitorV2 + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + sin.readString(), // id + sin.readLong(), // version + sin.readLong(), // seqNo + sin.readLong(), // primaryTerm + MonitorV2.readFrom(sin) // monitorV2 + ) + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(id) + out.writeLong(version) + out.writeLong(seqNo) + out.writeLong(primaryTerm) + MonitorV2.writeTo(out, monitorV2) + } + + @Throws(IOException::class) + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + return builder.startObject() + .field(_ID, id) + .field(_VERSION, version) + .field(_SEQ_NO, seqNo) + .field(_PRIMARY_TERM, primaryTerm) + .field(MONITOR_V2_FIELD, monitorV2) + .endObject() + } + + companion object { + const val MONITOR_V2_FIELD = "monitor_v2" + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt new file mode 100644 index 00000000..589f185d --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -0,0 +1,108 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import java.time.Instant +import org.opensearch.commons.alerting.model.Monitor.Companion +import org.opensearch.commons.alerting.model.Monitor.Companion.INPUTS_FIELD +import org.opensearch.commons.alerting.model.PPLMonitor.Companion.PPL_MONITOR_TYPE +import org.opensearch.commons.alerting.model.Trigger.Type +import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID +import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION +import org.opensearch.commons.alerting.util.nonOptionalTimeField +import org.opensearch.commons.alerting.util.optionalTimeField +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.XContent +import org.opensearch.core.xcontent.XContentBuilder +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils + +// TODO: maybe make this abstract class? put init block logic here? +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 labels: Map? + val triggers: List + + 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 type name + const val MONITOR_V2_TYPE = "monitor_v2" + + // field names + const val NAME_FIELD = "name" + const val MONITOR_TYPE_FIELD = "monitor_type" + 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 LABELS_FIELD = "labels" + const val TRIGGERS_FIELD = "triggers" + + // default values + const val NO_ID = "" + const val NO_VERSION = 1L + + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun parse(xcp: XContentParser): MonitorV2 { + /* + TODO: this default implementation is short-term and inextensible + a correct implementation should 1) scan for monitor type field + 2) delegate to the parse function of the MonitorV2 implementation, + just like how TriggerV2 interface does it. + The problem is the (internal) monitor type field is at the same + level as all the other monitor fields, which means we would need some + way of parsing the same XContent twice + possible work around: require monitor type to be very first field + */ + return PPLMonitor.parse(xcp) + } + + fun readFrom(sin: StreamInput): MonitorV2 { + val monitorType = sin.readEnum(MonitorV2Type::class.java) + return when (monitorType) { + 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(MonitorV2.MonitorV2Type.PPL_MONITOR) + monitorV2.writeTo(out) + } + } + } + + @Suppress("UNCHECKED_CAST") + fun convertLabelsMap(map: Map): Map { + if (map.values.all { it is String }) { + return map as Map + } else { + throw ClassCastException("at least one value in the map was not a string: $map") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt new file mode 100644 index 00000000..12e855b9 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt @@ -0,0 +1,69 @@ +package org.opensearch.commons.alerting.model + +import java.time.Instant +import org.opensearch.commons.alerting.alerts.AlertError +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 periodStart: Instant + val periodEnd: Instant + val triggerResults: Map + + enum class MonitorV2RunResultType() { + PPL_MONITOR_RUN_RESULT; + +// override fun toString(): String { +// return value +// } + +// companion object { +// fun enumFromString(value: String): MonitorV2Type? { +// return MonitorV2Type.entries.find { it.value == value } +// } +// } + } + + /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ + fun alertError(): AlertError? { + if (error != null) { + return AlertError(Instant.now(), "Failed running monitor:\n${error!!.userErrorMessage()}") + } + + return null + } + + companion object { + const val MONITOR_NAME_FIELD = "monitor_name" + const val ERROR_FIELD = "error" + const val PERIOD_START_FIELD = "period_start" + const val PERIOD_END_FIELD = "period_end" + 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) + } + } + } + + @Suppress("UNCHECKED_CAST") + fun suppressWarning(map: MutableMap?): Map { + return map as Map + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt new file mode 100644 index 00000000..37c206bc --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -0,0 +1,239 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import java.time.Instant +import org.opensearch.Version +import org.opensearch.commons.alerting.model.Monitor.Companion +import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.LABELS_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_TYPE_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_V2_TYPE +import org.opensearch.commons.alerting.model.MonitorV2.Companion.NAME_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_ID +import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_VERSION +import org.opensearch.commons.alerting.model.MonitorV2.Companion.SCHEDULE_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.TRIGGERS_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.convertLabelsMap +import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID +import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION +import org.opensearch.commons.alerting.util.instant +import org.opensearch.commons.alerting.util.nonOptionalTimeField +import org.opensearch.commons.alerting.util.optionalTimeField +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 + +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 lastUpdateTime: Instant, + override val enabledTime: Instant?, + override val labels: Map = emptyMap(), + override val triggers: List, + val query: String +) : MonitorV2 { + + // specify scheduled job type + override val type = MONITOR_V2_TYPE + + override fun fromDocument(id: String, version: Long): PPLMonitor = copy(id = id, version = version) + + init { + // for checking trigger ID uniqueness + val triggerIds = mutableSetOf() + triggers.forEach { trigger -> + require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique." } + } + + if (enabled) { + requireNotNull(enabledTime) + } else { + require(enabledTime == null) + } + + triggers.forEach { trigger -> + require(trigger is PPLTrigger) { "Incompatible trigger [${trigger.id}] for monitor type [$PPL_MONITOR_TYPE]" } + } + + // TODO: create setting for max triggers and check for max triggers here + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + id = sin.readString(), + version = sin.readLong(), + name = sin.readString(), + enabled = sin.readBoolean(), + schedule = Schedule.readFrom(sin), + lastUpdateTime = sin.readInstant(), + enabledTime = sin.readOptionalInstant(), + labels = sin.readMap()?.let { convertLabelsMap(it) } ?: emptyMap(), + triggers = sin.readList(TriggerV2::readFrom), + query = sin.readString() + ) + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + builder.field(NAME_FIELD, name) + // include monitor type field despite it not being a class field to differentiate + // PPL monitor from other monitor types in alerting config system index + builder.field(MONITOR_TYPE_FIELD, PPL_MONITOR_TYPE) + builder.field(ENABLED_FIELD, enabled) + builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) + builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) + builder.field(LABELS_FIELD, labels) + builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) + builder.field(QUERY_FIELD, query) + builder.endObject() + 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) + } + out.writeInstant(lastUpdateTime) + out.writeOptionalInstant(enabledTime) + out.writeMap(labels) + out.writeVInt(triggers.size) + triggers.forEach { + out.writeEnum(TriggerV2.TriggerV2Type.PPL_TRIGGER) + it.writeTo(out) + } + out.writeString(query) + } + + fun asTemplateArg(): Map { + return mapOf( + _ID to id, + _VERSION to version, + NAME_FIELD to name, + ENABLED_FIELD to enabled, + SCHEDULE_FIELD to schedule, + LAST_UPDATE_TIME_FIELD to lastUpdateTime.toEpochMilli(), + ENABLED_TIME_FIELD to enabledTime?.toEpochMilli(), + LABELS_FIELD to labels, + TRIGGERS_FIELD to triggers, + QUERY_FIELD to query + ) + } + + companion object { + // monitor type name + const val PPL_MONITOR_TYPE = "ppl_monitor" + + // field names + 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 monitorType: String = PPL_MONITOR_TYPE + var enabled = true + var schedule: Schedule? = null + val lastUpdateTime: Instant = Instant.now() // set time of update or first creation as lastUpdateTime + var enabledTime: Instant? = null + var labels: Map = emptyMap() + val triggers: MutableList = mutableListOf() + 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() + MONITOR_TYPE_FIELD -> monitorType = xcp.text() + ENABLED_FIELD -> enabled = xcp.booleanValue() + SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) + ENABLED_TIME_FIELD -> enabledTime = xcp.instant() + LABELS_FIELD -> labels = xcp.map() + TRIGGERS_FIELD -> { + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_ARRAY, + xcp.currentToken(), + xcp + ) + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + triggers.add(PPLTrigger.parseInner(xcp)) + } + } + QUERY_FIELD -> query = xcp.text() + } + } + + /* validations */ + + // TODO: add validations for throttle actions time range + // (see alerting's TransportIndexMonitorAction.validateActionThrottle) + + // ensure MonitorV2 XContent being parsed by PPLMonitor class is PPL Monitor type + if (monitorType != PPL_MONITOR_TYPE) { + throw IllegalArgumentException("Invalid monitor type: $monitorType") + } + + // ensure there's at least 1 trigger + if (triggers.isEmpty()) { + throw IllegalArgumentException("Monitor must include at least 1 trigger") + } + + // 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 + } + + // check if all label key,values are String,String, throw exception otherwise + try { + labels = convertLabelsMap(labels) + } catch (e: ClassCastException) { + throw IllegalArgumentException("invalid maps field, please ensure all labels are strings") + } + + // check for required fields + requireNotNull(name) { "Monitor name is null" } + requireNotNull(schedule) { "Schedule is null" } + requireNotNull(query) { "Query is null" } + + /* return PPLMonitor */ + return PPLMonitor( + id, + version, + name, + enabled, + schedule, + lastUpdateTime, + enabledTime, + labels, + triggers, + query + ) + } + +// @JvmStatic +// @Throws(IOException::class) +// fun readFrom(sin: StreamInput): PPLMonitor { +// return PPLMonitor(sin) +// } + } +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt new file mode 100644 index 00000000..64379ac6 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt @@ -0,0 +1,73 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import java.time.Instant +import org.opensearch.commons.alerting.alerts.AlertError +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.ERROR_FIELD +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.MONITOR_NAME_FIELD +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_END_FIELD +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_START_FIELD +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.TRIGGER_RESULTS_FIELD +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.suppressWarning +import org.opensearch.commons.alerting.util.nonOptionalTimeField +import org.opensearch.commons.alerting.util.optionalTimeField +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 + +data class PPLMonitorRunResult( + override val monitorName: String, + override val error: Exception?, + override val periodStart: Instant, + override val periodEnd: Instant, + override val triggerResults: Map, + val pplQueryResults: String // TODO: will likely be a different type like Map or JsonObject +) : MonitorV2RunResult { + + @Throws(IOException::class) + @Suppress("UNCHECKED_CAST") + constructor(sin: StreamInput) : this( + sin.readString(), // monitorName + sin.readException(), // error + sin.readInstant(), // periodStart + sin.readInstant(), // periodEnd + suppressWarning(sin.readMap()) as Map, // triggerResults + sin.readString() // pplQueryResults + ) + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + builder.field(MONITOR_NAME_FIELD, monitorName) + builder.nonOptionalTimeField(PERIOD_START_FIELD, periodStart) + builder.nonOptionalTimeField(PERIOD_END_FIELD, periodEnd) + 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.writeInstant(periodStart) + out.writeInstant(periodEnd) + out.writeMap(triggerResults) + out.writeString(pplQueryResults) + } + + // TODO: does this need any PPLMonitor specific logic, or can this just be deleted + override fun alertError(): AlertError? { + if (error != null) { + return AlertError(Instant.now(), "Failed running monitor:\n${error.userErrorMessage()}") + } + + return null + } + + companion object { + const val PPL_QUERY_RESULTS_FIELD = "ppl_query_results" + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt new file mode 100644 index 00000000..09933e0d --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -0,0 +1,220 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import org.opensearch.common.CheckedFunction +import org.opensearch.common.UUIDs +import org.opensearch.commons.alerting.model.TriggerV2.Companion.ACTIONS_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Companion.ID_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Companion.NAME_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Companion.SEVERITY_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Severity +import org.opensearch.commons.alerting.model.action.Action +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 + +data class PPLTrigger( + override val id: String = UUIDs.base64UUID(), + override val name: String, + override val severity: Severity, + override val actions: List, + val mode: TriggerMode, // result_set or per_result + // val suppress // TODO: potentially need to use OScore's TimeValue + val conditionType: ConditionType, + val numResultsCondition: NumResultsCondition?, + val numResultsValue: Long?, + val customCondition: String? +) : TriggerV2 { + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + sin.readString(), // id + sin.readString(), // name + sin.readEnum(Severity::class.java), // severity + sin.readList(::Action), // actions + sin.readEnum(TriggerMode::class.java), // trigger mode + // TODO: add validation to ensure numResultsCondition and numResultsValue or customCondition are non-null based on condition type? + sin.readEnum(ConditionType::class.java), // condition type + // TODO: can updated StreamInput be picked up so we can use readOptionalEnum? + 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.writeCollection(actions) + out.writeEnum(mode) + out.writeEnum(conditionType) + out.writeBoolean(numResultsCondition != null) // TODO: look for built-in writeOptionalEnum support + if (numResultsCondition != null) out.writeEnum(numResultsCondition) + out.writeOptionalLong(numResultsValue) + out.writeOptionalString(customCondition) + } + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params?): XContentBuilder { + builder.startObject() + builder.startObject(PPL_TRIGGER_FIELD) + builder.field(ID_FIELD, id) + builder.field(NAME_FIELD, name) + builder.field(MODE_FIELD, mode.value) + builder.field(CONDITION_TYPE_FIELD, conditionType.value) + builder.field(NUM_RESULTS_CONDITION_FIELD, numResultsCondition?.value) + builder.field(NUM_RESULTS_VALUE_FIELD, numResultsValue) + builder.field(CUSTOM_CONDITION_FIELD, customCondition) + builder.field(SEVERITY_FIELD, severity.value) + builder.field(ACTIONS_FIELD, actions.toTypedArray()) + builder.endObject() + builder.endObject() + return builder + } + + fun asTemplateArg(): Map { + return mapOf( + ID_FIELD to id, + NAME_FIELD to name, + 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, + SEVERITY_FIELD to severity.value, + ACTIONS_FIELD to actions.map { it.asTemplateArg() } + ) + } + + 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 { + const val PPL_TRIGGER_FIELD = "ppl_trigger" + + 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 mode: TriggerMode? = null + var conditionType: ConditionType? = null + var numResultsCondition: NumResultsCondition? = null + var numResultsValue: Long? = null + var customCondition: String? = null + val actions: MutableList = mutableListOf() + + /* 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) { + ID_FIELD -> id = xcp.text() + NAME_FIELD -> name = xcp.text() + SEVERITY_FIELD -> { + val enumMatchResult = Severity.enumFromString(xcp.text()) + ?: throw IllegalArgumentException("Invalid value for $SEVERITY_FIELD. Supported values are ${Severity.entries.map { it.value }}") + severity = enumMatchResult + } + MODE_FIELD -> { + val enumMatchResult = TriggerMode.enumFromString(xcp.text()) + ?: throw IllegalArgumentException("Invalid value for $MODE_FIELD. Supported values are ${TriggerMode.entries.map { it.value }}") + mode = enumMatchResult + } + CONDITION_TYPE_FIELD -> { + val enumMatchResult = ConditionType.enumFromString(xcp.text()) + ?: throw IllegalArgumentException("Invalid value for $CONDITION_TYPE_FIELD. Supported values are ${ConditionType.entries.map { it.value }}") + conditionType = enumMatchResult + } + NUM_RESULTS_CONDITION_FIELD -> numResultsCondition = NumResultsCondition.enumFromString(xcp.text()) + NUM_RESULTS_VALUE_FIELD -> numResultsValue = xcp.longValue() + CUSTOM_CONDITION_FIELD -> customCondition = xcp.text() + ACTIONS_FIELD -> { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + actions.add(Action.parse(xcp)) + } + } + } + } + + /* validations */ + requireNotNull(name) { "Trigger name is null" } + requireNotNull(severity) { "Severity is null" } + requireNotNull(mode) { "Trigger mode is null" } + requireNotNull(conditionType) { "Trigger condition type is null" } + + when (conditionType) { + ConditionType.NUMBER_OF_RESULTS -> { + requireNotNull(numResultsCondition) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_CONDITION_FIELD cannot be null" } + requireNotNull(numResultsValue) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_VALUE_FIELD cannot be null" } + } + ConditionType.CUSTOM -> { + requireNotNull(customCondition) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $CUSTOM_CONDITION_FIELD cannot be null" } + } + } + + // 3. prepare and return PPLTrigger object + return PPLTrigger( + id, + name, + severity, + actions, + mode, + conditionType, + numResultsCondition, + numResultsValue, + customCondition, + ) + } + } +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt new file mode 100644 index 00000000..ce71f6bb --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt @@ -0,0 +1,73 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import java.time.Instant +import org.opensearch.commons.alerting.alerts.AlertError +import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.ERROR_FIELD +import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.NAME_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 + +data class PPLTriggerRunResult( + override var triggerName: String, + override var error: Exception?, + open var triggered: Boolean, // TODO: may need to change this based on whether trigger mode is result set or per result + open var actionResults: MutableMap = mutableMapOf() +) : TriggerV2RunResult { + + @Throws(IOException::class) + @Suppress("UNCHECKED_CAST") + constructor(sin: StreamInput) : this( + triggerName = sin.readString(), + error = sin.readException(), + triggered = sin.readBoolean(), + actionResults = sin.readMap() as MutableMap + ) + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + builder.field(NAME_FIELD, triggerName) + + builder.field(TRIGGERED_FIELD, triggered) + builder.field(ACTION_RESULTS_FIELD, actionResults as Map) + + val msg = error?.message + builder.field(ERROR_FIELD, msg) + builder.endObject() + + return builder + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(triggerName) + out.writeException(error) + out.writeBoolean(triggered) + out.writeMap(actionResults as Map) + } + + override fun alertError(): AlertError? { + if (error != null) { + return AlertError(Instant.now(), "Failed evaluating trigger:\n${error!!.userErrorMessage()}") + } + for (actionResult in actionResults.values) { + if (actionResult.error != null) { + return AlertError(Instant.now(), "Failed running action:\n${actionResult.error.userErrorMessage()}") + } + } + return null + } + + companion object { + const val TRIGGERED_FIELD = "triggered" + const val ACTION_RESULTS_FIELD = "action_results" + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): TriggerRunResult { + return QueryLevelTriggerRunResult(sin) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt new file mode 100644 index 00000000..11d731ba --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -0,0 +1,79 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import org.opensearch.commons.alerting.model.PPLTrigger.Companion.PPL_TRIGGER_FIELD +import org.opensearch.commons.alerting.model.action.Action +import org.opensearch.commons.notifications.model.BaseModel +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils + +interface TriggerV2 : BaseModel { + + val id: String + val name: String + val severity: Severity + // val expires // TODO: potentially need to use OScore's TimeValue + 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"), + LOW("low"), + MEDIUM("medium"), + HIGH("high"), + CRITICAL("critical"); + + companion object { + fun enumFromString(value: String): Severity? { + return entries.find { it.value == value } + } + } + } + + companion object { + const val ID_FIELD = "id" + const val NAME_FIELD = "name" + const val SEVERITY_FIELD = "severity" + const val SUPPRESS_FIELD = "suppress" + const val EXPIRES_FIELD = "expires" + const val ACTIONS_FIELD = "actions" + +// @Throws(IOException::class) +// fun parse(xcp: XContentParser): TriggerV2 { +// // TODO: dead code until a MonitorV2 interface level parse() that delegates by monitor type is implemented +// val trigger: TriggerV2 +// +// val triggerV2TypeNames = TriggerV2Type.entries.map { it.value } +// +// XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) +// XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) +// +// if (!triggerV2TypeNames.contains(xcp.currentName())) { +// throw IllegalArgumentException("Invalid trigger type ${xcp.currentName()}") +// } +// +// XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) +// trigger = xcp.namedObject(TriggerV2::class.java, xcp.currentName(), null) +// XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, xcp.nextToken(), xcp) +// +// return trigger +// } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): TriggerV2 { + return when (val type = sin.readEnum(TriggerV2Type::class.java)) { + TriggerV2Type.PPL_TRIGGER -> PPLTrigger(sin) + else -> throw IllegalStateException("Unexpected input [$type] when reading TriggerV2") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt new file mode 100644 index 00000000..1573e48b --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt @@ -0,0 +1,28 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import java.time.Instant +import org.opensearch.commons.alerting.alerts.AlertError +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 + +interface TriggerV2RunResult : Writeable, ToXContent { + + val triggerName: String + val error: Exception? + + /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ + fun alertError(): AlertError? { + if (error != null) { + return AlertError(Instant.now(), "Failed evaluating trigger:\n${error!!.userErrorMessage()}") + } + return null + } + + companion object { + const val NAME_FIELD = "name" + const val ERROR_FIELD = "error" + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt b/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt index 887e8430..ce82b19d 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt @@ -66,6 +66,11 @@ fun XContentBuilder.optionalUsernameField(name: String, user: User?): XContentBu return this.field(name, user.name) } +fun XContentBuilder.nonOptionalTimeField(name: String, instant: Instant): XContentBuilder { + // second name as readableName should be different than first name + return this.timeField(name, "${name}_in_millis", instant.toEpochMilli()) +} + fun XContentBuilder.optionalTimeField(name: String, instant: Instant?): XContentBuilder { if (instant == null) { return nullField(name) From ddd3fd895cf95eadd0514c69c427d8cac152fe08 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 7 Aug 2025 13:50:47 -0700 Subject: [PATCH 02/30] dummy rest handler to ping sql/ppl plugin in transport layer --- build.gradle | 8 +- .../commons/ppl/action/PPLQueryAction.java | 18 +++ .../ppl/action/TransportPPLQueryRequest.java | 138 ++++++++++++++++++ .../ppl/action/TransportPPLQueryResponse.java | 51 +++++++ .../commons/ppl/format/ErrorFormatter.java | 59 ++++++++ .../opensearch/commons/ppl/format/Format.java | 61 ++++++++ .../ppl/format/JsonResponseFormatter.java | 66 +++++++++ .../commons/ppl/format/ResponseFormatter.java | 33 +++++ .../commons/ppl/serde/DataSourceType.java | 61 ++++++++ .../commons/ppl/serde/SerializeUtils.java | 50 +++++++ .../commons/ppl/util/PPLQueryRequest.java | 73 +++++++++ .../ppl/util/PPLQueryRequestFactory.java | 118 +++++++++++++++ .../alerting/action/AlertingActions.kt | 5 + .../alerting/action/PingPPLSQLRequest.kt | 33 +++++ .../alerting/action/PingPPLSQLResponse.kt | 36 +++++ .../commons/ppl/PPLPluginInterface.kt | 51 +++++++ 16 files changed, 860 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java create mode 100644 src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java create mode 100644 src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java create mode 100644 src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java create mode 100644 src/main/java/org/opensearch/commons/ppl/format/Format.java create mode 100644 src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java create mode 100644 src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java create mode 100644 src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java create mode 100644 src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java create mode 100644 src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java create mode 100644 src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt create mode 100644 src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt diff --git a/build.gradle b/build.gradle index d26722b3..d615e0c5 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ plugins { id 'java-library' id 'maven-publish' id 'com.diffplug.spotless' version '6.25.0' + id "io.freefair.lombok" version "8.14" } repositories { @@ -90,7 +91,12 @@ dependencies { testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.cronutils:cron-utils:9.1.6" testImplementation "commons-validator:commons-validator:1.7" - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.4' + implementation 'org.json:json:20240303' + implementation "com.google.guava:guava:33.3.0-jre" + implementation 'com.google.code.gson:gson:2.10.1' + compileOnly 'org.projectlombok:lombok:1.18.38' + annotationProcessor 'org.projectlombok:lombok:1.18.38' ktlint "com.pinterest:ktlint:0.47.1" } diff --git a/src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java b/src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java new file mode 100644 index 00000000..e1e8ef36 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.action; + +import org.opensearch.action.ActionType; + +public class PPLQueryAction extends ActionType { + // Internal Action which is not used for public facing RestAPIs. + public static final String NAME = "cluster:admin/opensearch/ppl"; + public static final PPLQueryAction INSTANCE = new PPLQueryAction(); + + private PPLQueryAction() { + super(NAME, TransportPPLQueryResponse::new); + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java new file mode 100644 index 00000000..5b515547 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java @@ -0,0 +1,138 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.action; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Locale; +import java.util.Optional; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.json.JSONObject; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.commons.ppl.format.Format; +import org.opensearch.commons.ppl.format.JsonResponseFormatter; +import org.opensearch.commons.ppl.util.PPLQueryRequest; +import org.opensearch.core.common.io.stream.InputStreamStreamInput; +import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +//private val log = LogManager.getLogger(TransportExecuteMonitorAction::class.java) + +@RequiredArgsConstructor +public class TransportPPLQueryRequest extends ActionRequest { + public static final TransportPPLQueryRequest NULL = new TransportPPLQueryRequest("", null, ""); + private final String pplQuery; + @Getter private final JSONObject jsonContent; + + @Getter private final String path; + + @Getter private String format = ""; + + @Setter + @Getter + @Accessors(fluent = true) + private boolean sanitize = true; + + @Setter + @Getter + @Accessors(fluent = true) + private JsonResponseFormatter.Style style = JsonResponseFormatter.Style.COMPACT; + + /** Constructor of TransportPPLQueryRequest from PPLQueryRequest. */ + public TransportPPLQueryRequest(PPLQueryRequest pplQueryRequest) { + pplQuery = pplQueryRequest.getRequest(); + jsonContent = pplQueryRequest.getJsonContent(); + path = pplQueryRequest.getPath(); + format = pplQueryRequest.getFormat(); + sanitize = pplQueryRequest.sanitize(); + style = pplQueryRequest.style(); + } + + /** Constructor of TransportPPLQueryRequest from StreamInput. */ + public TransportPPLQueryRequest(StreamInput in) throws IOException { + super(in); + pplQuery = in.readOptionalString(); + format = in.readOptionalString(); + String jsonContentString = in.readOptionalString(); + jsonContent = jsonContentString != null ? new JSONObject(jsonContentString) : null; + path = in.readOptionalString(); + sanitize = in.readBoolean(); + style = in.readEnum(JsonResponseFormatter.Style.class); + } + + /** Re-create the object from the actionRequest. */ + public static TransportPPLQueryRequest fromActionRequest(final ActionRequest actionRequest) { + if (actionRequest instanceof TransportPPLQueryRequest) { + return (TransportPPLQueryRequest) actionRequest; + } + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { + actionRequest.writeTo(osso); + try (InputStreamStreamInput input = + new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { + return new TransportPPLQueryRequest(input); + } + } catch (IOException e) { + throw new IllegalArgumentException( + "failed to parse ActionRequest into TransportPPLQueryRequest", e); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(pplQuery); + out.writeOptionalString(format); + out.writeOptionalString(jsonContent != null ? jsonContent.toString() : null); + out.writeOptionalString(path); + out.writeBoolean(sanitize); + out.writeEnum(style); + } + + public String getRequest() { + return pplQuery; + } + + /** + * Check if request is to explain rather than execute the query. + * + * @return true if it is an explain request + */ + public boolean isExplainRequest() { + return path.endsWith("/_explain"); + } + + /** Decide on the formatter by the requested format. */ + public Format format() { + Optional optionalFormat = Format.of(format); + if (optionalFormat.isPresent()) { + return optionalFormat.get(); + } else { + throw new IllegalArgumentException( + String.format(Locale.ROOT, "response in %s format is not supported.", format)); + } + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + /** Convert to PPLQueryRequest. */ + public PPLQueryRequest toPPLQueryRequest() { + PPLQueryRequest pplQueryRequest = new PPLQueryRequest(pplQuery, jsonContent, path, format); + pplQueryRequest.sanitize(sanitize); + pplQueryRequest.style(style); + return pplQueryRequest; + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java new file mode 100644 index 00000000..469a7212 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.action; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.InputStreamStreamInput; +import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +@RequiredArgsConstructor +public class TransportPPLQueryResponse extends ActionResponse { + @Getter public final String result; + + public TransportPPLQueryResponse(StreamInput in) throws IOException { + super(in); + result = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(result); + } + + public static TransportPPLQueryResponse fromActionResponse(ActionResponse actionResponse) { + if (actionResponse instanceof TransportPPLQueryResponse) { + return (TransportPPLQueryResponse) actionResponse; + } + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { + actionResponse.writeTo(osso); + try (StreamInput input = + new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { + return new TransportPPLQueryResponse(input); + } + } catch (IOException e) { + throw new UncheckedIOException( + "failed to parse ActionResponse into TransportPPLQueryResponse", e); + } + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java new file mode 100644 index 00000000..2e54ed8f --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.format; + +import com.google.gson.Gson; +import java.security.AccessController; +import java.security.PrivilegedAction; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.UtilityClass; +import org.opensearch.commons.ppl.serde.SerializeUtils; + +@UtilityClass +public class ErrorFormatter { + + private static final Gson PRETTY_PRINT_GSON = + AccessController.doPrivileged( + (PrivilegedAction) + () -> + SerializeUtils.getGsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .create()); + private static final Gson GSON = + AccessController.doPrivileged( + (PrivilegedAction) + () -> SerializeUtils.getGsonBuilder().disableHtmlEscaping().create()); + + /** Util method to format {@link Throwable} response to JSON string in compact printing. */ + public static String compactFormat(Throwable t) { + JsonError error = new ErrorFormatter.JsonError(t.getClass().getSimpleName(), t.getMessage()); + return compactJsonify(error); + } + + /** Util method to format {@link Throwable} response to JSON string in pretty printing. */ + public static String prettyFormat(Throwable t) { + JsonError error = new ErrorFormatter.JsonError(t.getClass().getSimpleName(), t.getMessage()); + return prettyJsonify(error); + } + + public static String compactJsonify(Object jsonObject) { + return AccessController.doPrivileged((PrivilegedAction) () -> GSON.toJson(jsonObject)); + } + + public static String prettyJsonify(Object jsonObject) { + return AccessController.doPrivileged( + (PrivilegedAction) () -> PRETTY_PRINT_GSON.toJson(jsonObject)); + } + + @RequiredArgsConstructor + @Getter + public static class JsonError { + private final String type; + private final String reason; + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/format/Format.java b/src/main/java/org/opensearch/commons/ppl/format/Format.java new file mode 100644 index 00000000..957ea22e --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/format/Format.java @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.format; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum Format { + JDBC("jdbc"), + CSV("csv"), + RAW("raw"), + VIZ("viz"), + // format of explain response + SIMPLE("simple"), + STANDARD("standard"), + EXTENDED("extended"), + COST("cost"); + + @Getter private final String formatName; + + public static final Map RESPONSE_FORMATS; + + public static final Map EXPLAIN_FORMATS; + + static { + ImmutableMap.Builder builder; + builder = new ImmutableMap.Builder<>(); + builder.put(JDBC.formatName, JDBC); + builder.put(CSV.formatName, CSV); + builder.put(RAW.formatName, RAW); + builder.put(VIZ.formatName, VIZ); + RESPONSE_FORMATS = builder.build(); + + builder = new ImmutableMap.Builder<>(); + builder.put(SIMPLE.formatName, SIMPLE); + builder.put(STANDARD.formatName, STANDARD); + builder.put(EXTENDED.formatName, EXTENDED); + builder.put(COST.formatName, COST); + EXPLAIN_FORMATS = builder.build(); + } + + public static Optional of(String formatName) { + String format = Strings.isNullOrEmpty(formatName) ? "jdbc" : formatName.toLowerCase(Locale.ROOT); + return Optional.ofNullable(RESPONSE_FORMATS.getOrDefault(format, null)); + } + + public static Optional ofExplain(String formatName) { + String format = Strings.isNullOrEmpty(formatName) ? "standard" : formatName.toLowerCase(Locale.ROOT); + return Optional.ofNullable(EXPLAIN_FORMATS.getOrDefault(format, null)); + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java new file mode 100644 index 00000000..9abc54e0 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.format; + +import static org.opensearch.commons.ppl.format.ErrorFormatter.compactFormat; +import static org.opensearch.commons.ppl.format.ErrorFormatter.compactJsonify; +import static org.opensearch.commons.ppl.format.ErrorFormatter.prettyFormat; +import static org.opensearch.commons.ppl.format.ErrorFormatter.prettyJsonify; +import static org.opensearch.commons.ppl.format.JsonResponseFormatter.Style.PRETTY; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import lombok.RequiredArgsConstructor; + +/** + * Abstract class for all JSON formatter. + * + * @param response generic type which could be DQL or DML response + */ +@RequiredArgsConstructor +public abstract class JsonResponseFormatter implements ResponseFormatter { + + /** JSON format styles: pretty format or compact format without indent and space. */ + public enum Style { + PRETTY, + COMPACT + } + + /** JSON format style. */ + private final Style style; + + public static final String CONTENT_TYPE = "application/json; charset=UTF-8"; + + @Override + public String format(R response) { + return jsonify(buildJsonObject(response)); + } + + @Override + public String format(Throwable t) { + return AccessController.doPrivileged( + (PrivilegedAction) () -> (style == PRETTY) ? prettyFormat(t) : compactFormat(t)); + } + + public String contentType() { + return CONTENT_TYPE; + } + + /** + * Build JSON object to generate response json string. + * + * @param response response + * @return json object for response + */ + protected abstract Object buildJsonObject(R response); + + protected String jsonify(Object jsonObject) { + return AccessController.doPrivileged( + (PrivilegedAction) + () -> (style == PRETTY) ? prettyJsonify(jsonObject) : compactJsonify(jsonObject)); + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java new file mode 100644 index 00000000..1e257680 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.format; + +/** Response formatter to format response to different formats. */ +public interface ResponseFormatter { + + /** + * Format response into string in expected format. + * + * @param response response + * @return string with response content formatted + */ + String format(R response); + + /** + * Format an exception into string. + * + * @param t exception occurred + * @return string with exception content formatted + */ + String format(Throwable t); + + /** + * Getter for the content type header of the response. + * + * @return string + */ + String contentType(); +} diff --git a/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java b/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java new file mode 100644 index 00000000..b8f32758 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.serde; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@EqualsAndHashCode +public class DataSourceType { + public static final DataSourceType PROMETHEUS = new DataSourceType("PROMETHEUS"); + public static final DataSourceType OPENSEARCH = new DataSourceType("OPENSEARCH"); + public static final DataSourceType S3GLUE = new DataSourceType("S3GLUE"); + public static final DataSourceType SECURITY_LAKE = new DataSourceType("SECURITY_LAKE"); + + // Map from uppercase DataSourceType name to DataSourceType object + private static final Map knownValues = new HashMap<>(); + + static { + register(PROMETHEUS, OPENSEARCH, S3GLUE, SECURITY_LAKE); + } + + private final String name; + + public String name() { + return name; + } + + @Override + public String toString() { + return name; + } + + /** Register DataSourceType to be used in fromString method */ + public static void register(DataSourceType... dataSourceTypes) { + for (DataSourceType type : dataSourceTypes) { + String upperCaseName = type.name().toUpperCase(Locale.ROOT); + if (!knownValues.containsKey(upperCaseName)) { + knownValues.put(type.name().toUpperCase(Locale.ROOT), type); + } else { + throw new IllegalArgumentException( + "DataSourceType with name " + type.name() + " already exists"); + } + } + } + + public static DataSourceType fromString(String name) { + String upperCaseName = name.toUpperCase(Locale.ROOT); + if (knownValues.containsKey(upperCaseName)) { + return knownValues.get(upperCaseName); + } else { + throw new IllegalArgumentException("No DataSourceType with name " + name + " found"); + } + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java b/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java new file mode 100644 index 00000000..9c0f72b3 --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.serde; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class SerializeUtils { + private static class DataSourceTypeSerializer implements JsonSerializer { + @Override + public JsonElement serialize( + DataSourceType dataSourceType, + Type type, + JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(dataSourceType.name()); + } + } + + private static class DataSourceTypeDeserializer implements JsonDeserializer { + @Override + public DataSourceType deserialize( + JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) + throws JsonParseException { + return DataSourceType.fromString(jsonElement.getAsString()); + } + } + + public static GsonBuilder getGsonBuilder() { + return new GsonBuilder() + .registerTypeAdapter(DataSourceType.class, new DataSourceTypeSerializer()) + .registerTypeAdapter(DataSourceType.class, new DataSourceTypeDeserializer()); + } + + public static Gson buildGson() { + return getGsonBuilder().create(); + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java new file mode 100644 index 00000000..ab0e635b --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.util; + +import java.util.Locale; +import java.util.Optional; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.json.JSONObject; +import org.opensearch.commons.ppl.format.Format; +import org.opensearch.commons.ppl.format.JsonResponseFormatter; + +public class PPLQueryRequest { + + private static final String DEFAULT_PPL_PATH = "/_plugins/_ppl"; + + public static final PPLQueryRequest NULL = new PPLQueryRequest("", null, DEFAULT_PPL_PATH, ""); + + private final String pplQuery; + @Getter private final JSONObject jsonContent; + @Getter private final String path; + @Getter private String format = ""; + + @Setter + @Getter + @Accessors(fluent = true) + private boolean sanitize = true; + + @Setter + @Getter + @Accessors(fluent = true) + private JsonResponseFormatter.Style style = JsonResponseFormatter.Style.COMPACT; + + public PPLQueryRequest(String pplQuery, JSONObject jsonContent, String path) { + this(pplQuery, jsonContent, path, ""); + } + + /** Constructor of PPLQueryRequest. */ + public PPLQueryRequest(String pplQuery, JSONObject jsonContent, String path, String format) { + this.pplQuery = pplQuery; + this.jsonContent = jsonContent; + this.path = Optional.ofNullable(path).orElse(DEFAULT_PPL_PATH); + this.format = format; + } + + public String getRequest() { + return pplQuery; + } + + /** + * Check if request is to explain rather than execute the query. + * + * @return true if it is a explain request + */ + public boolean isExplainRequest() { + return path.endsWith("/_explain"); + } + + /** Decide on the formatter by the requested format. */ + public Format format() { + Optional optionalFormat = Format.of(format); + if (optionalFormat.isPresent()) { + return optionalFormat.get(); + } else { + throw new IllegalArgumentException( + String.format(Locale.ROOT, "response in %s format is not supported.", format)); + } + } +} diff --git a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java new file mode 100644 index 00000000..402f6adb --- /dev/null +++ b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.commons.ppl.util; + +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import org.json.JSONException; +import org.json.JSONObject; +import org.opensearch.commons.ppl.format.Format; +import org.opensearch.commons.ppl.format.JsonResponseFormatter; +import org.opensearch.rest.RestRequest; + +/** Factory of {@link PPLQueryRequest}. */ +public class PPLQueryRequestFactory { + private static final String PPL_URL_PARAM_KEY = "ppl"; + private static final String PPL_FIELD_NAME = "query"; + private static final String QUERY_PARAMS_FORMAT = "format"; + private static final String QUERY_PARAMS_SANITIZE = "sanitize"; + private static final String DEFAULT_RESPONSE_FORMAT = "jdbc"; + private static final String DEFAULT_EXPLAIN_FORMAT = "standard"; + private static final String QUERY_PARAMS_PRETTY = "pretty"; + + /** + * Build {@link PPLQueryRequest} from {@link RestRequest}. + * + * @param request {@link PPLQueryRequest} + * @return {@link RestRequest} + */ + public static PPLQueryRequest getPPLRequest(RestRequest request) { + switch (request.method()) { + case GET: + return parsePPLRequestFromUrl(request); + case POST: + return parsePPLRequestFromPayload(request); + default: + throw new IllegalArgumentException( + "OpenSearch PPL doesn't supported HTTP " + request.method().name()); + } + } + + private static PPLQueryRequest parsePPLRequestFromUrl(RestRequest restRequest) { + String ppl; + + ppl = restRequest.param(PPL_URL_PARAM_KEY); + if (ppl == null) { + throw new IllegalArgumentException("Cannot find ppl parameter from the URL"); + } + return new PPLQueryRequest(ppl, null, restRequest.path()); + } + + private static PPLQueryRequest parsePPLRequestFromPayload(RestRequest restRequest) { + String content = restRequest.content().utf8ToString(); + JSONObject jsonContent; + Format format = getFormat(restRequest.params(), restRequest.rawPath()); + boolean pretty = getPrettyOption(restRequest.params()); + try { + jsonContent = new JSONObject(content); + PPLQueryRequest pplRequest = + new PPLQueryRequest( + jsonContent.getString(PPL_FIELD_NAME), + jsonContent, + restRequest.path(), + format.getFormatName()); + // set sanitize option if csv format + if (format.equals(Format.CSV)) { + pplRequest.sanitize(getSanitizeOption(restRequest.params())); + } + // set pretty option + if (pretty) { + pplRequest.style(JsonResponseFormatter.Style.PRETTY); + } + return pplRequest; + } catch (JSONException e) { + throw new IllegalArgumentException("Failed to parse request payload", e); + } + } + + private static Format getFormat(Map requestParams, String path) { + String formatName = + requestParams.containsKey(QUERY_PARAMS_FORMAT) + ? requestParams.get(QUERY_PARAMS_FORMAT).toLowerCase(Locale.ROOT) + : isExplainRequest(path) ? DEFAULT_EXPLAIN_FORMAT : DEFAULT_RESPONSE_FORMAT; + Optional optionalFormat = + isExplainRequest(path) ? Format.ofExplain(formatName) : Format.of(formatName); + if (optionalFormat.isPresent()) { + return optionalFormat.get(); + } else { + throw new IllegalArgumentException( + "Failed to create executor due to unknown response format: " + formatName); + } + } + + private static boolean isExplainRequest(String path) { + return path != null && path.endsWith("/_explain"); + } + + private static boolean getSanitizeOption(Map requestParams) { + if (requestParams.containsKey(QUERY_PARAMS_SANITIZE)) { + return Boolean.parseBoolean(requestParams.get(QUERY_PARAMS_SANITIZE)); + } + return true; + } + + private static boolean getPrettyOption(Map requestParams) { + if (requestParams.containsKey(QUERY_PARAMS_PRETTY)) { + String prettyValue = requestParams.get(QUERY_PARAMS_PRETTY); + if (prettyValue.isEmpty()) { + return true; + } + return Boolean.parseBoolean(prettyValue); + } + return false; + } +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt index 302d35d3..841247ee 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt @@ -25,6 +25,11 @@ object AlertingActions { const val INDEX_COMMENT_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/write" const val SEARCH_COMMENTS_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/search" const val DELETE_COMMENT_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/delete" + const val PING_PPL_SQL_ACTION_NAME = "cluster:admin/opensearch/ping_ppl_sql" + + @JvmField + val PING_PPL_SQL_ACTION_TYPE = + ActionType(PING_PPL_SQL_ACTION_NAME, ::PingPPLSQLResponse) // Alerting V2 const val INDEX_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/write" diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt new file mode 100644 index 00000000..bbe7bd04 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt @@ -0,0 +1,33 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.commons.ppl.action.TransportPPLQueryRequest +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.search.fetch.subphase.FetchSourceContext + +class PingPPLSQLRequest : ActionRequest { + val queryRequest: TransportPPLQueryRequest + + constructor( + queryRequest: TransportPPLQueryRequest + ) : super() { + this.queryRequest = queryRequest + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + TransportPPLQueryRequest(sin) + ) + + override fun validate(): ActionRequestValidationException? { + return null + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + queryRequest.writeTo(out) + } +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt new file mode 100644 index 00000000..e2755147 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt @@ -0,0 +1,36 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.commons.notifications.action.BaseResponse +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 kotlin.jvm.Throws + +class PingPPLSQLResponse : BaseResponse { + var queryResponse: String? + + @Throws(IOException::class) + constructor(queryResponse: String?) : super() { + this.queryResponse = queryResponse + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : super() { + this.queryResponse = sin.readOptionalString() + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeOptionalString(queryResponse) + } + + @Throws(IOException::class) + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + .field("query_response", queryResponse) + .endObject() + return builder + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt new file mode 100644 index 00000000..ff93fbdf --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt @@ -0,0 +1,51 @@ +package org.opensearch.commons.ppl + +import org.opensearch.commons.notifications.action.BaseResponse +import org.opensearch.commons.ppl.action.PPLQueryAction +import org.opensearch.commons.ppl.action.TransportPPLQueryRequest +import org.opensearch.commons.ppl.action.TransportPPLQueryResponse +import org.opensearch.commons.utils.recreateObject +import org.opensearch.core.action.ActionListener +import org.opensearch.core.action.ActionResponse +import org.opensearch.core.common.io.stream.Writeable +import org.opensearch.transport.client.node.NodeClient + +/** + * Various transport action plugin interfaces for the SQL/PPL plugin + */ +object PPLPluginInterface { + fun executeQuery( + client: NodeClient, + request: TransportPPLQueryRequest, + listener: ActionListener + ) { + client.execute( + PPLQueryAction.INSTANCE, + request, + wrapActionListener(listener) { response -> recreateObject(response) { TransportPPLQueryResponse(it) } } + ) + } + + /** + * Wrap action listener on concrete response class by a new created one on ActionResponse. + * This is required because the response may be loaded by different classloader across plugins. + * The onResponse(ActionResponse) avoids type cast exception and give a chance to recreate + * the response object. + */ + @Suppress("UNCHECKED_CAST") + private fun wrapActionListener( + listener: ActionListener, + recreate: (Writeable) -> Response + ): ActionListener { + return object : ActionListener { + override fun onResponse(response: ActionResponse) { + val recreated = recreate(response) + listener.onResponse(recreated) + } + + override fun onFailure(exception: java.lang.Exception) { + listener.onFailure(exception) + } + } as ActionListener + } +} \ No newline at end of file From ad41b6bad8ef512d8a8ca8bc3e4b4c4387b53f2d Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 27 Aug 2025 15:36:29 -0700 Subject: [PATCH 03/30] adding a bunch of models --- .../alerting/action/AlertingActions.kt | 15 +- .../alerting/action/DeleteMonitorV2Request.kt | 34 +++ .../action/DeleteMonitorV2Response.kt | 38 +++ .../alerting/action/PingPPLSQLResponse.kt | 36 --- ...QLRequest.kt => SearchMonitorV2Request.kt} | 17 +- .../commons/alerting/model/AlertV2.kt | 258 ++++++++++++++++++ .../commons/alerting/model/MonitorV2.kt | 19 +- .../commons/alerting/model/PPLMonitor.kt | 29 +- .../alerting/model/PPLMonitorRunResult.kt | 7 +- .../commons/alerting/model/PPLTrigger.kt | 130 +++++++-- .../alerting/model/PPLTriggerRunResult.kt | 16 +- .../commons/alerting/model/ScheduledJob.kt | 3 + .../commons/alerting/model/TriggerV2.kt | 11 +- .../alerting/model/TriggerV2RunResult.kt | 2 + 14 files changed, 517 insertions(+), 98 deletions(-) create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt rename src/main/kotlin/org/opensearch/commons/alerting/action/{PingPPLSQLRequest.kt => SearchMonitorV2Request.kt} (60%) create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt index 841247ee..c3ac43c6 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt @@ -25,14 +25,11 @@ object AlertingActions { const val INDEX_COMMENT_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/write" const val SEARCH_COMMENTS_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/search" const val DELETE_COMMENT_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/delete" - const val PING_PPL_SQL_ACTION_NAME = "cluster:admin/opensearch/ping_ppl_sql" - - @JvmField - val PING_PPL_SQL_ACTION_TYPE = - ActionType(PING_PPL_SQL_ACTION_NAME, ::PingPPLSQLResponse) // Alerting V2 const val INDEX_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/write" + const val SEARCH_MONITORS_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/search" + const val DELETE_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/delete" @JvmField val INDEX_MONITOR_ACTION_TYPE = @@ -101,4 +98,12 @@ object AlertingActions { @JvmField val INDEX_MONITOR_V2_ACTION_TYPE = ActionType(INDEX_MONITOR_V2_ACTION_NAME, ::IndexMonitorV2Response) + + @JvmField + val SEARCH_MONITORS_V2_ACTION_TYPE = + ActionType(SEARCH_MONITORS_V2_ACTION_NAME, ::SearchResponse) + + @JvmField + val DELETE_MONITOR_V2_ACTION_TYPE = + ActionType(DELETE_MONITOR_V2_ACTION_NAME, ::DeleteMonitorV2Response) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt new file mode 100644 index 00000000..dee79394 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt @@ -0,0 +1,34 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.action.support.WriteRequest +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput + +class DeleteMonitorV2Request : ActionRequest { + val monitorV2Id: String + val refreshPolicy: WriteRequest.RefreshPolicy + + constructor(monitorV2Id: String, refreshPolicy: WriteRequest.RefreshPolicy) : super() { + this.monitorV2Id = monitorV2Id + this.refreshPolicy = refreshPolicy + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + monitorV2Id = sin.readString(), + refreshPolicy = WriteRequest.RefreshPolicy.readFrom(sin) + ) + + override fun validate(): ActionRequestValidationException? { + return null + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(monitorV2Id) + refreshPolicy.writeTo(out) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt new file mode 100644 index 00000000..228a561e --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt @@ -0,0 +1,38 @@ +package org.opensearch.commons.alerting.action + +import org.opensearch.commons.alerting.util.IndexUtils +import org.opensearch.commons.notifications.action.BaseResponse +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 + +class DeleteMonitorV2Response : BaseResponse { + var id: String + var version: Long + + constructor( + id: String, + version: Long + ) : super() { + this.id = id + this.version = version + } + + constructor(sin: StreamInput) : this( + sin.readString(), // id + sin.readLong() // version + ) + + override fun writeTo(out: StreamOutput) { + out.writeString(id) + out.writeLong(version) + } + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + return builder.startObject() + .field(IndexUtils._ID, id) + .field(IndexUtils._VERSION, version) + .endObject() + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt deleted file mode 100644 index e2755147..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLResponse.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.opensearch.commons.alerting.action - -import java.io.IOException -import org.opensearch.commons.notifications.action.BaseResponse -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 kotlin.jvm.Throws - -class PingPPLSQLResponse : BaseResponse { - var queryResponse: String? - - @Throws(IOException::class) - constructor(queryResponse: String?) : super() { - this.queryResponse = queryResponse - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : super() { - this.queryResponse = sin.readOptionalString() - } - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeOptionalString(queryResponse) - } - - @Throws(IOException::class) - override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { - builder.startObject() - .field("query_response", queryResponse) - .endObject() - return builder - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt similarity index 60% rename from src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt rename to src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt index bbe7bd04..32cbf48d 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/PingPPLSQLRequest.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt @@ -3,23 +3,22 @@ package org.opensearch.commons.alerting.action import java.io.IOException import org.opensearch.action.ActionRequest import org.opensearch.action.ActionRequestValidationException -import org.opensearch.commons.ppl.action.TransportPPLQueryRequest +import org.opensearch.action.search.SearchRequest import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput -import org.opensearch.search.fetch.subphase.FetchSourceContext -class PingPPLSQLRequest : ActionRequest { - val queryRequest: TransportPPLQueryRequest +class SearchMonitorV2Request : ActionRequest { + val searchRequest: SearchRequest constructor( - queryRequest: TransportPPLQueryRequest + searchRequest: SearchRequest ) : super() { - this.queryRequest = queryRequest + this.searchRequest = searchRequest } @Throws(IOException::class) constructor(sin: StreamInput) : this( - TransportPPLQueryRequest(sin) + searchRequest = SearchRequest(sin) ) override fun validate(): ActionRequestValidationException? { @@ -28,6 +27,6 @@ class PingPPLSQLRequest : ActionRequest { @Throws(IOException::class) override fun writeTo(out: StreamOutput) { - queryRequest.writeTo(out) + searchRequest.writeTo(out) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt new file mode 100644 index 00000000..b3e5d620 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -0,0 +1,258 @@ +package org.opensearch.commons.alerting.model + +import java.io.IOException +import java.time.Instant +import org.opensearch.common.lucene.uid.Versions +import org.opensearch.commons.alerting.alerts.AlertError +import org.opensearch.commons.alerting.model.Alert.Companion.ACKNOWLEDGED_TIME_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.ACTION_EXECUTION_RESULTS_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_HISTORY_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_ID_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_VERSION_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.END_TIME_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.ERROR_MESSAGE_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.EXECUTION_ID_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.LAST_NOTIFICATION_TIME_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_ID_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_NAME_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_VERSION_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.NO_ID +import org.opensearch.commons.alerting.model.Alert.Companion.NO_VERSION +import org.opensearch.commons.alerting.model.Alert.Companion.SCHEMA_VERSION_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.SEVERITY_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.START_TIME_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.STATE_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_ID_FIELD +import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_NAME_FIELD +import org.opensearch.commons.alerting.model.Alert.State +import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION +import org.opensearch.commons.alerting.util.instant +import org.opensearch.commons.alerting.util.optionalTimeField +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 + +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 state: State, // TODO: potentially delete for stateless alerts, all stateless alerts are ACTIVE + val startTime: Instant, // TODO: potentially delete for stateless alerts + val endTime: Instant? = null, // TODO: potentially delete for stateless alerts + val expirationTime: Instant?, + val lastNotificationTime: Instant? = null, + val acknowledgedTime: Instant? = null, + val errorMessage: String? = null, + val errorHistory: List, + val severity: String, + val actionExecutionResults: List, + 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(), + state = sin.readEnum(State::class.java), + startTime = sin.readInstant(), + endTime = sin.readOptionalInstant(), + expirationTime = sin.readOptionalInstant(), + lastNotificationTime = sin.readOptionalInstant(), + acknowledgedTime = sin.readOptionalInstant(), + errorMessage = sin.readOptionalString(), + errorHistory = sin.readList(::AlertError), + severity = sin.readString(), + actionExecutionResults = sin.readList(::ActionExecutionResult), + 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.writeEnum(state) + out.writeInstant(startTime) + out.writeOptionalInstant(endTime) + out.writeOptionalInstant(expirationTime) + out.writeOptionalInstant(lastNotificationTime) + out.writeOptionalInstant(acknowledgedTime) + out.writeOptionalString(errorMessage) + out.writeCollection(errorHistory) + out.writeString(severity) + out.writeCollection(actionExecutionResults) + out.writeOptionalString(executionId) + } + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + .field(ALERT_ID_FIELD, id) + .field(ALERT_VERSION_FIELD, version) + .field(MONITOR_ID_FIELD, monitorId) + .field(SCHEMA_VERSION_FIELD, schemaVersion) + .field(MONITOR_VERSION_FIELD, monitorVersion) + .field(MONITOR_NAME_FIELD, monitorName) + .field(EXECUTION_ID_FIELD, executionId) + .field(TRIGGER_ID_FIELD, triggerId) + .field(TRIGGER_NAME_FIELD, triggerName) + .field(STATE_FIELD, state) + .field(ERROR_MESSAGE_FIELD, errorMessage) + .field(ALERT_HISTORY_FIELD, errorHistory.toTypedArray()) + .field(SEVERITY_FIELD, severity) + .field(ACTION_EXECUTION_RESULTS_FIELD, actionExecutionResults.toTypedArray()) + .optionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) + .optionalTimeField(START_TIME_FIELD, startTime) + .optionalTimeField(LAST_NOTIFICATION_TIME_FIELD, lastNotificationTime) + .optionalTimeField(END_TIME_FIELD, endTime) + .optionalTimeField(ACKNOWLEDGED_TIME_FIELD, acknowledgedTime) + .endObject() + +// if (!secure) { +// builder.optionalUserField(MONITOR_USER_FIELD, monitorUser) +// } + + return builder + } + + fun asTemplateArg(): Map { + return mapOf( + ACKNOWLEDGED_TIME_FIELD to acknowledgedTime?.toEpochMilli(), + ALERT_ID_FIELD to id, + ALERT_VERSION_FIELD to version, + END_TIME_FIELD to endTime?.toEpochMilli(), + ERROR_MESSAGE_FIELD to errorMessage, + EXECUTION_ID_FIELD to executionId, + LAST_NOTIFICATION_TIME_FIELD to lastNotificationTime?.toEpochMilli(), + EXPIRATION_TIME_FIELD to expirationTime?.toEpochMilli(), + SEVERITY_FIELD to severity, + START_TIME_FIELD to startTime.toEpochMilli(), + STATE_FIELD to state.toString(), + ) + } + + companion object { + const val EXPIRATION_TIME_FIELD = "expiration_time" + + @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 state: State + lateinit var startTime: Instant + lateinit var severity: String + var expirationTime: Instant? = null + var endTime: Instant? = null + var lastNotificationTime: Instant? = null + var acknowledgedTime: Instant? = null + var errorMessage: String? = null + var executionId: String? = null + val errorHistory: MutableList = mutableListOf() + val actionExecutionResults: MutableList = mutableListOf() + + ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = xcp.currentName() + xcp.nextToken() + + when (fieldName) { + MONITOR_ID_FIELD -> monitorId = xcp.text() + SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() + MONITOR_NAME_FIELD -> monitorName = xcp.text() + MONITOR_VERSION_FIELD -> monitorVersion = xcp.longValue() +// MONITOR_USER_FIELD -> +// monitorUser = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { +// null +// } else { +// User.parse(xcp) +// } + TRIGGER_ID_FIELD -> triggerId = xcp.text() + STATE_FIELD -> state = State.valueOf(xcp.text()) + TRIGGER_NAME_FIELD -> triggerName = xcp.text() + START_TIME_FIELD -> startTime = requireNotNull(xcp.instant()) + END_TIME_FIELD -> endTime = xcp.instant() + EXPIRATION_TIME_FIELD -> expirationTime = xcp.instant() + LAST_NOTIFICATION_TIME_FIELD -> lastNotificationTime = xcp.instant() + ACKNOWLEDGED_TIME_FIELD -> acknowledgedTime = xcp.instant() + ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull() + EXECUTION_ID_FIELD -> executionId = xcp.textOrNull() + ALERT_HISTORY_FIELD -> { + ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + errorHistory.add(AlertError.parse(xcp)) + } + } + SEVERITY_FIELD -> severity = xcp.text() + ACTION_EXECUTION_RESULTS_FIELD -> { + ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + actionExecutionResults.add(ActionExecutionResult.parse(xcp)) + } + } + } + } + + return AlertV2( + id = id, + version = version, + schemaVersion = schemaVersion, + monitorId = requireNotNull(monitorId), + monitorName = requireNotNull(monitorName), + monitorVersion = monitorVersion, +// monitorUser = monitorUser, + triggerId = requireNotNull(triggerId), + triggerName = requireNotNull(triggerName), + state = requireNotNull(state), + startTime = requireNotNull(startTime), + endTime = endTime, + expirationTime = expirationTime, + lastNotificationTime = lastNotificationTime, + acknowledgedTime = acknowledgedTime, + errorMessage = errorMessage, + errorHistory = errorHistory, + severity = severity, + actionExecutionResults = actionExecutionResults, + executionId = executionId, + ) + } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): AlertV2 { + return AlertV2(sin) + } + } +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index 589f185d..e818c4bf 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -2,6 +2,7 @@ package org.opensearch.commons.alerting.model import java.io.IOException import java.time.Instant +import org.opensearch.common.CheckedFunction import org.opensearch.commons.alerting.model.Monitor.Companion import org.opensearch.commons.alerting.model.Monitor.Companion.INPUTS_FIELD import org.opensearch.commons.alerting.model.PPLMonitor.Companion.PPL_MONITOR_TYPE @@ -10,15 +11,17 @@ import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION import org.opensearch.commons.alerting.util.nonOptionalTimeField 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.XContent import org.opensearch.core.xcontent.XContentBuilder import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils -// TODO: maybe make this abstract class? put init block logic here? +// TODO: maybe make this abstract class? put init block logic here for all monitors? interface MonitorV2 : ScheduledJob { override val id: String override val version: Long @@ -44,9 +47,12 @@ interface MonitorV2 : ScheduledJob { } } + fun asTemplateArg(): Map + companion object { - // scheduled job type name - const val MONITOR_V2_TYPE = "monitor_v2" + // scheduled job field names + const val TYPE_FIELD = "type" + const val MONITOR_V2_TYPE = "monitor_v2" // scheduled job type is MonitorV2 // field names const val NAME_FIELD = "name" @@ -62,6 +68,12 @@ interface MonitorV2 : ScheduledJob { const val NO_ID = "" const val NO_VERSION = 1L + val XCONTENT_REGISTRY = NamedXContentRegistry.Entry( + ScheduledJob::class.java, + ParseField(MONITOR_V2_TYPE), + CheckedFunction { parse(it) } + ) + @JvmStatic @JvmOverloads @Throws(IOException::class) @@ -75,6 +87,7 @@ interface MonitorV2 : ScheduledJob { level as all the other monitor fields, which means we would need some way of parsing the same XContent twice possible work around: require monitor type to be very first field + if first monitor type field is absent, assume ppl monitor as default */ return PPLMonitor.parse(xcp) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index 37c206bc..b62a4ee3 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -2,8 +2,11 @@ package org.opensearch.commons.alerting.model import java.io.IOException import java.time.Instant +import org.apache.logging.log4j.LogManager import org.opensearch.Version +import org.opensearch.common.CheckedFunction import org.opensearch.commons.alerting.model.Monitor.Companion +import org.opensearch.commons.alerting.model.Monitor.Companion.MONITOR_TYPE import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.LABELS_FIELD @@ -15,19 +18,24 @@ import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_ID import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_VERSION import org.opensearch.commons.alerting.model.MonitorV2.Companion.SCHEDULE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.TRIGGERS_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.TYPE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.convertLabelsMap import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION import org.opensearch.commons.alerting.util.instant import org.opensearch.commons.alerting.util.nonOptionalTimeField 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 +private val logger = LogManager.getLogger(PPLMonitor::class.java) + data class PPLMonitor( override val id: String = NO_ID, override val version: Long = NO_VERSION, @@ -82,10 +90,20 @@ data class PPLMonitor( override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { builder.startObject() - builder.field(NAME_FIELD, name) + + // 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(MONITOR_V2_TYPE) + } + builder.field(TYPE_FIELD, MONITOR_V2_TYPE) + // include monitor type field despite it not being a class field to differentiate // PPL monitor from other monitor types in alerting config system index builder.field(MONITOR_TYPE_FIELD, PPL_MONITOR_TYPE) + + builder.field(NAME_FIELD, name) + builder.field(SCHEDULE_FIELD, schedule) builder.field(ENABLED_FIELD, enabled) builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) @@ -93,6 +111,12 @@ data class PPLMonitor( builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) builder.field(QUERY_FIELD, query) builder.endObject() + + // if ScheduledJob metadata was added, end the extra object layer that was created + if (params.paramAsBoolean("with_type", false)) { + builder.endObject() + } + return builder } @@ -118,7 +142,7 @@ data class PPLMonitor( out.writeString(query) } - fun asTemplateArg(): Map { + override fun asTemplateArg(): Map { return mapOf( _ID to id, _VERSION to version, @@ -154,6 +178,7 @@ data class PPLMonitor( val triggers: MutableList = mutableListOf() var query: String? = null + /* parse */ XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt index 64379ac6..9b2b6e8e 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt @@ -10,7 +10,6 @@ import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.TRIGGER_RESULTS_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.suppressWarning import org.opensearch.commons.alerting.util.nonOptionalTimeField -import org.opensearch.commons.alerting.util.optionalTimeField import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput import org.opensearch.core.xcontent.ToXContent @@ -22,7 +21,7 @@ data class PPLMonitorRunResult( override val periodStart: Instant, override val periodEnd: Instant, override val triggerResults: Map, - val pplQueryResults: String // TODO: will likely be a different type like Map or JsonObject + val pplQueryResults: Map> // key: trigger id, value: query results ) : MonitorV2RunResult { @Throws(IOException::class) @@ -33,7 +32,7 @@ data class PPLMonitorRunResult( sin.readInstant(), // periodStart sin.readInstant(), // periodEnd suppressWarning(sin.readMap()) as Map, // triggerResults - sin.readString() // pplQueryResults + sin.readMap() as Map> // pplQueryResults // TODO: if this works, delete suppressWarning call above ) override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { @@ -55,7 +54,7 @@ data class PPLMonitorRunResult( out.writeInstant(periodStart) out.writeInstant(periodEnd) out.writeMap(triggerResults) - out.writeString(pplQueryResults) + out.writeMap(pplQueryResults) } // TODO: does this need any PPLMonitor specific logic, or can this just be deleted diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 09933e0d..79951dba 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -1,14 +1,23 @@ package org.opensearch.commons.alerting.model import java.io.IOException +import java.time.Instant +import org.apache.logging.log4j.LogManager import org.opensearch.common.CheckedFunction import org.opensearch.common.UUIDs +import org.opensearch.common.unit.TimeValue import org.opensearch.commons.alerting.model.TriggerV2.Companion.ACTIONS_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Companion.EXPIRE_FIELD import org.opensearch.commons.alerting.model.TriggerV2.Companion.ID_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Companion.LAST_TRIGGERED_FIELD import org.opensearch.commons.alerting.model.TriggerV2.Companion.NAME_FIELD import org.opensearch.commons.alerting.model.TriggerV2.Companion.SEVERITY_FIELD +import org.opensearch.commons.alerting.model.TriggerV2.Companion.SUPPRESS_FIELD import org.opensearch.commons.alerting.model.TriggerV2.Severity 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.commons.authuser.User import org.opensearch.core.ParseField import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput @@ -18,13 +27,17 @@ import org.opensearch.core.xcontent.XContentBuilder import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils +private val logger = LogManager.getLogger(PPLTrigger::class.java) + data class PPLTrigger( override val id: String = UUIDs.base64UUID(), override val name: String, override val severity: Severity, + override val suppressDuration: TimeValue?, + override val expireDuration: TimeValue?, + override var lastTriggeredTime: Instant?, override val actions: List, val mode: TriggerMode, // result_set or per_result - // val suppress // TODO: potentially need to use OScore's TimeValue val conditionType: ConditionType, val numResultsCondition: NumResultsCondition?, val numResultsValue: Long?, @@ -36,6 +49,11 @@ data class PPLTrigger( sin.readString(), // id sin.readString(), // name sin.readEnum(Severity::class.java), // severity + // parseTimeValue() is typically used to parse OpenSearch settings + // the second param is supposed to accept a setting name, but here we're passing in our own name + TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_SUPPRESS_SETTING_NAME), // suppressDuration + TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_EXPIRE_SETTING_NAME), // expireDuration + sin.readOptionalInstant(), // lastTriggeredTime sin.readList(::Action), // actions sin.readEnum(TriggerMode::class.java), // trigger mode // TODO: add validation to ensure numResultsCondition and numResultsValue or customCondition are non-null based on condition type? @@ -51,11 +69,21 @@ data class PPLTrigger( out.writeString(id) out.writeString(name) out.writeEnum(severity) + + out.writeBoolean(suppressDuration != null) + suppressDuration?.let { out.writeString(suppressDuration.toHumanReadableString(0)) } + + out.writeBoolean(expireDuration != null) + expireDuration?.let { out.writeString(expireDuration.toHumanReadableString(0)) } + + out.writeOptionalInstant(lastTriggeredTime) out.writeCollection(actions) out.writeEnum(mode) out.writeEnum(conditionType) + out.writeBoolean(numResultsCondition != null) // TODO: look for built-in writeOptionalEnum support - if (numResultsCondition != null) out.writeEnum(numResultsCondition) + numResultsCondition?.let { out.writeEnum(numResultsCondition) } + out.writeOptionalLong(numResultsValue) out.writeOptionalString(customCondition) } @@ -65,13 +93,16 @@ data class PPLTrigger( builder.startObject(PPL_TRIGGER_FIELD) builder.field(ID_FIELD, id) builder.field(NAME_FIELD, name) - builder.field(MODE_FIELD, mode.value) - builder.field(CONDITION_TYPE_FIELD, conditionType.value) - builder.field(NUM_RESULTS_CONDITION_FIELD, numResultsCondition?.value) - builder.field(NUM_RESULTS_VALUE_FIELD, numResultsValue) - builder.field(CUSTOM_CONDITION_FIELD, customCondition) builder.field(SEVERITY_FIELD, severity.value) + builder.field(SUPPRESS_FIELD, suppressDuration?.toHumanReadableString(0)) + builder.field(EXPIRE_FIELD, expireDuration?.toHumanReadableString(0)) + 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() builder.endObject() return builder @@ -81,13 +112,15 @@ data class PPLTrigger( return mapOf( ID_FIELD to id, NAME_FIELD to name, + SEVERITY_FIELD to severity.value, + SUPPRESS_FIELD to suppressDuration?.toHumanReadableString(0), + EXPIRE_FIELD to expireDuration?.toHumanReadableString(0), + 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, - SEVERITY_FIELD to severity.value, - ACTIONS_FIELD to actions.map { it.asTemplateArg() } ) } @@ -123,61 +156,100 @@ data class PPLTrigger( } 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" + // mock setting name used when parsing TimeValue + private const val PLACEHOLDER_SUPPRESS_SETTING_NAME = "ppl_trigger_suppress_duration" + private const val PLACEHOLDER_EXPIRE_SETTING_NAME = "ppl_trigger_expire_duration" + 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 suppressDuration: TimeValue? = null + var expireDuration: TimeValue? = null + 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 - val actions: MutableList = mutableListOf() /* parse */ - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) // outer trigger object start + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) // ppl_trigger field name + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner trigger object start while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { val fieldName = xcp.currentName() xcp.nextToken() + // TODO: if e.g. trigger is num results but user explicitly passes in custom_condition = null, parse fails because + // TODO: it tries to parse a text value from null when (fieldName) { ID_FIELD -> id = xcp.text() NAME_FIELD -> name = xcp.text() SEVERITY_FIELD -> { - val enumMatchResult = Severity.enumFromString(xcp.text()) - ?: throw IllegalArgumentException("Invalid value for $SEVERITY_FIELD. Supported values are ${Severity.entries.map { it.value }}") + 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 enumMatchResult = TriggerMode.enumFromString(xcp.text()) - ?: throw IllegalArgumentException("Invalid value for $MODE_FIELD. Supported values are ${TriggerMode.entries.map { it.value }}") + 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 enumMatchResult = ConditionType.enumFromString(xcp.text()) - ?: throw IllegalArgumentException("Invalid value for $CONDITION_TYPE_FIELD. Supported values are ${ConditionType.entries.map { it.value }}") + 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 -> numResultsCondition = NumResultsCondition.enumFromString(xcp.text()) + NUM_RESULTS_CONDITION_FIELD -> { + 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 -> numResultsValue = xcp.longValue() CUSTOM_CONDITION_FIELD -> customCondition = xcp.text() + SUPPRESS_FIELD -> { + suppressDuration = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + val input = xcp.text() + TimeValue.parseTimeValue(input, PLACEHOLDER_SUPPRESS_SETTING_NAME) // throws IllegalArgumentException if there's parsing error + } + } + EXPIRE_FIELD -> { + expireDuration = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + val input = xcp.text() + TimeValue.parseTimeValue(input, PLACEHOLDER_EXPIRE_SETTING_NAME) // throws IllegalArgumentException if there's parsing error + } + } + LAST_TRIGGERED_FIELD -> lastTriggeredTime = xcp.instant() ACTIONS_FIELD -> { XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { @@ -187,19 +259,24 @@ data class PPLTrigger( } } + XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, xcp.nextToken(), xcp) // end of outer trigger object + /* validations */ - requireNotNull(name) { "Trigger name is null" } - requireNotNull(severity) { "Severity is null" } - requireNotNull(mode) { "Trigger mode is null" } - requireNotNull(conditionType) { "Trigger condition type is null" } + 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" } when (conditionType) { ConditionType.NUMBER_OF_RESULTS -> { - requireNotNull(numResultsCondition) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_CONDITION_FIELD cannot be null" } - requireNotNull(numResultsValue) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_VALUE_FIELD cannot be null" } + requireNotNull(numResultsCondition) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_CONDITION_FIELD must be included" } + requireNotNull(numResultsValue) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_VALUE_FIELD must be included" } + require(customCondition == null) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $CUSTOM_CONDITION_FIELD must not be included" } } ConditionType.CUSTOM -> { - requireNotNull(customCondition) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $CUSTOM_CONDITION_FIELD cannot be null" } + requireNotNull(customCondition) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $CUSTOM_CONDITION_FIELD must be included" } + require(numResultsCondition == null) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $NUM_RESULTS_CONDITION_FIELD must not be included" } + require(numResultsValue == null) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $NUM_RESULTS_VALUE_FIELD must not be included" } } } @@ -208,6 +285,9 @@ data class PPLTrigger( id, name, severity, + suppressDuration, + expireDuration, + lastTriggeredTime, actions, mode, conditionType, diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt index ce71f6bb..7de608ca 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt @@ -5,6 +5,7 @@ import java.time.Instant import org.opensearch.commons.alerting.alerts.AlertError import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.ERROR_FIELD import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.NAME_FIELD +import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.TRIGGERED_FIELD import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput import org.opensearch.core.xcontent.ToXContent @@ -12,39 +13,35 @@ import org.opensearch.core.xcontent.XContentBuilder data class PPLTriggerRunResult( override var triggerName: String, + override var triggered: Boolean, // TODO: may need to change this based on whether trigger mode is result set or per result override var error: Exception?, - open var triggered: Boolean, // TODO: may need to change this based on whether trigger mode is result set or per result - open var actionResults: MutableMap = mutableMapOf() + var actionResults: MutableMap = mutableMapOf() ) : TriggerV2RunResult { @Throws(IOException::class) @Suppress("UNCHECKED_CAST") constructor(sin: StreamInput) : this( triggerName = sin.readString(), - error = sin.readException(), triggered = sin.readBoolean(), + error = sin.readException(), actionResults = sin.readMap() as MutableMap ) 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.field(ACTION_RESULTS_FIELD, actionResults as Map) - - val msg = error?.message - builder.field(ERROR_FIELD, msg) builder.endObject() - return builder } @Throws(IOException::class) override fun writeTo(out: StreamOutput) { out.writeString(triggerName) - out.writeException(error) out.writeBoolean(triggered) + out.writeException(error) out.writeMap(actionResults as Map) } @@ -61,7 +58,6 @@ data class PPLTriggerRunResult( } companion object { - const val TRIGGERED_FIELD = "triggered" const val ACTION_RESULTS_FIELD = "action_results" @JvmStatic diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt index cf8417c2..b37e037c 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt @@ -7,6 +7,9 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant +import org.apache.logging.log4j.LogManager + +private val log = LogManager.getLogger(ScheduledJob::class.java) interface ScheduledJob : BaseModel { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt index 11d731ba..d29e3fba 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -1,19 +1,21 @@ package org.opensearch.commons.alerting.model import java.io.IOException +import java.time.Instant +import org.opensearch.common.unit.TimeValue import org.opensearch.commons.alerting.model.PPLTrigger.Companion.PPL_TRIGGER_FIELD import org.opensearch.commons.alerting.model.action.Action import org.opensearch.commons.notifications.model.BaseModel import org.opensearch.core.common.io.stream.StreamInput -import org.opensearch.core.xcontent.XContentParser -import org.opensearch.core.xcontent.XContentParserUtils interface TriggerV2 : BaseModel { val id: String val name: String val severity: Severity - // val expires // TODO: potentially need to use OScore's TimeValue + val suppressDuration: TimeValue? // TODO: move to MonitorV2 definition + val expireDuration: TimeValue? // TODO: move to MonitorV2 definition + var lastTriggeredTime: Instant? val actions: List enum class TriggerV2Type(val value: String) { @@ -43,7 +45,8 @@ interface TriggerV2 : BaseModel { const val NAME_FIELD = "name" const val SEVERITY_FIELD = "severity" const val SUPPRESS_FIELD = "suppress" - const val EXPIRES_FIELD = "expires" + const val LAST_TRIGGERED_FIELD = "last_triggered_time" + const val EXPIRE_FIELD = "expires" const val ACTIONS_FIELD = "actions" // @Throws(IOException::class) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt index 1573e48b..95a4bc40 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt @@ -11,6 +11,7 @@ import org.opensearch.core.xcontent.XContentBuilder interface TriggerV2RunResult : Writeable, ToXContent { val triggerName: String + val triggered: Boolean val error: Exception? /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ @@ -23,6 +24,7 @@ interface TriggerV2RunResult : Writeable, ToXContent { companion object { const val NAME_FIELD = "name" + const val TRIGGERED_FIELD = "triggered" const val ERROR_FIELD = "error" } } \ No newline at end of file From 8a48579a5b55c5602b27300459d7b25e9f0b1be4 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 2 Sep 2025 12:32:00 -0700 Subject: [PATCH 04/30] adding a bunch more models --- .../alerting/action/AlertingActions.kt | 5 ++ .../alerting/action/GetMonitorV2Request.kt | 47 +++++++++++ .../alerting/action/GetMonitorV2Response.kt | 75 ++++++++++++++++++ .../commons/alerting/model/MonitorV2.kt | 15 +--- .../commons/alerting/model/PPLMonitor.kt | 79 ++++++++++++------- .../commons/alerting/model/PPLTrigger.kt | 1 + 6 files changed, 182 insertions(+), 40 deletions(-) create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt index c3ac43c6..b985c5d6 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt @@ -28,6 +28,7 @@ object AlertingActions { // Alerting V2 const val INDEX_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/write" + const val GET_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/get" const val SEARCH_MONITORS_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/search" const val DELETE_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/delete" @@ -99,6 +100,10 @@ object AlertingActions { val INDEX_MONITOR_V2_ACTION_TYPE = ActionType(INDEX_MONITOR_V2_ACTION_NAME, ::IndexMonitorV2Response) + @JvmField + val GET_MONITOR_V2_ACTION_TYPE = + ActionType(GET_MONITOR_V2_ACTION_NAME, ::GetMonitorV2Response) + @JvmField val SEARCH_MONITORS_V2_ACTION_TYPE = ActionType(SEARCH_MONITORS_V2_ACTION_NAME, ::SearchResponse) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt new file mode 100644 index 00000000..42977871 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt @@ -0,0 +1,47 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.search.fetch.subphase.FetchSourceContext + +class GetMonitorV2Request : ActionRequest { + val monitorV2Id: String + val version: Long + val srcContext: FetchSourceContext? + + constructor( + monitorV2Id: String, + version: Long, + srcContext: FetchSourceContext? + ) : super() { + this.monitorV2Id = monitorV2Id + this.version = version + this.srcContext = srcContext + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + sin.readString(), // monitorV2Id + sin.readLong(), // version + if (sin.readBoolean()) { + FetchSourceContext(sin) // srcContext + } else { + null + } + ) + + override fun validate(): ActionRequestValidationException? { + return null + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(monitorV2Id) + out.writeLong(version) + out.writeBoolean(srcContext != null) + srcContext?.writeTo(out) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt new file mode 100644 index 00000000..72a7eb8b --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt @@ -0,0 +1,75 @@ +package org.opensearch.commons.alerting.action + +import java.io.IOException +import org.opensearch.commons.alerting.model.MonitorV2 +import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID +import org.opensearch.commons.alerting.util.IndexUtils.Companion._PRIMARY_TERM +import org.opensearch.commons.alerting.util.IndexUtils.Companion._SEQ_NO +import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION +import org.opensearch.commons.notifications.action.BaseResponse +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 + +class GetMonitorV2Response : BaseResponse { + var id: String + var version: Long + var seqNo: Long + var primaryTerm: Long + var monitorV2: MonitorV2? + + constructor( + id: String, + version: Long, + seqNo: Long, + primaryTerm: Long, + monitorV2: MonitorV2? + ) : super() { + this.id = id + this.version = version + this.seqNo = seqNo + this.primaryTerm = primaryTerm + this.monitorV2 = monitorV2 + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + id = sin.readString(), // id + version = sin.readLong(), // version + seqNo = sin.readLong(), // seqNo + primaryTerm = sin.readLong(), // primaryTerm + monitorV2 = if (sin.readBoolean()) { + MonitorV2.readFrom(sin) // monitorV2 + } else { + null + } + ) + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeString(id) + out.writeLong(version) + out.writeLong(seqNo) + out.writeLong(primaryTerm) + if (monitorV2 != null) { + out.writeBoolean(true) + monitorV2?.writeTo(out) + } else { + out.writeBoolean(false) + } + } + + @Throws(IOException::class) + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + .field(_ID, id) + .field(_VERSION, version) + .field(_SEQ_NO, seqNo) + .field(_PRIMARY_TERM, primaryTerm) + if (monitorV2 != null) { + builder.field("monitorV2", monitorV2) + } + return builder.endObject() + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index e818c4bf..e4e40fb2 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -30,9 +30,10 @@ 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 labels: Map? val triggers: List + fun asTemplateArg(): Map + enum class MonitorV2Type(val value: String) { PPL_MONITOR(PPL_MONITOR_TYPE); @@ -47,8 +48,6 @@ interface MonitorV2 : ScheduledJob { } } - fun asTemplateArg(): Map - companion object { // scheduled job field names const val TYPE_FIELD = "type" @@ -61,7 +60,6 @@ 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 LABELS_FIELD = "labels" const val TRIGGERS_FIELD = "triggers" // default values @@ -108,14 +106,5 @@ interface MonitorV2 : ScheduledJob { } } } - - @Suppress("UNCHECKED_CAST") - fun convertLabelsMap(map: Map): Map { - if (map.values.all { it is String }) { - return map as Map - } else { - throw ClassCastException("at least one value in the map was not a string: $map") - } - } } } \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index b62a4ee3..ec2ae37f 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -3,13 +3,8 @@ package org.opensearch.commons.alerting.model import java.io.IOException import java.time.Instant import org.apache.logging.log4j.LogManager -import org.opensearch.Version -import org.opensearch.common.CheckedFunction -import org.opensearch.commons.alerting.model.Monitor.Companion -import org.opensearch.commons.alerting.model.Monitor.Companion.MONITOR_TYPE import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.LABELS_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_TYPE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_V2_TYPE @@ -19,16 +14,16 @@ import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_VERSION import org.opensearch.commons.alerting.model.MonitorV2.Companion.SCHEDULE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.TRIGGERS_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.TYPE_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.convertLabelsMap +import org.opensearch.commons.alerting.model.PPLTrigger.Companion.NUM_RESULTS_CONDITION_FIELD +import org.opensearch.commons.alerting.model.PPLTrigger.NumResultsCondition +import org.opensearch.commons.alerting.model.PPLTrigger.TriggerMode import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION import org.opensearch.commons.alerting.util.instant import org.opensearch.commons.alerting.util.nonOptionalTimeField 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 @@ -36,6 +31,11 @@ import org.opensearch.core.xcontent.XContentParserUtils private val logger = LogManager.getLogger(PPLMonitor::class.java) +// TODO: probably change this to be called PPLSQLMonitor. A PPL Monitor and SQL Monitor +// TODO: would have the exact same functionality, except the choice of language +// TODO: when calling PPL/SQL plugin's execute API would be different. +// TODO: we dont need 2 different monitor types for that, just a simple if check +// TODO: for query language at monitor execution time data class PPLMonitor( override val id: String = NO_ID, override val version: Long = NO_VERSION, @@ -44,8 +44,8 @@ data class PPLMonitor( override val schedule: Schedule, override val lastUpdateTime: Instant, override val enabledTime: Instant?, - override val labels: Map = emptyMap(), override val triggers: List, + val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported val query: String ) : MonitorV2 { @@ -55,6 +55,11 @@ data class PPLMonitor( override fun fromDocument(id: String, version: Long): PPLMonitor = copy(id = id, version = version) init { + // SQL monitors are not yet supported + if (this.queryLanguage == QueryLanguage.SQL) { + throw IllegalStateException("Monitors with SQL queries are not supported") + } + // for checking trigger ID uniqueness val triggerIds = mutableSetOf() triggers.forEach { trigger -> @@ -83,8 +88,8 @@ data class PPLMonitor( schedule = Schedule.readFrom(sin), lastUpdateTime = sin.readInstant(), enabledTime = sin.readOptionalInstant(), - labels = sin.readMap()?.let { convertLabelsMap(it) } ?: emptyMap(), triggers = sin.readList(TriggerV2::readFrom), + queryLanguage = sin.readEnum(QueryLanguage::class.java), query = sin.readString() ) @@ -98,8 +103,7 @@ data class PPLMonitor( } builder.field(TYPE_FIELD, MONITOR_V2_TYPE) - // include monitor type field despite it not being a class field to differentiate - // PPL monitor from other monitor types in alerting config system index + // this field is ScheduledJob metadata, include despite it not being a class field builder.field(MONITOR_TYPE_FIELD, PPL_MONITOR_TYPE) builder.field(NAME_FIELD, name) @@ -107,8 +111,8 @@ data class PPLMonitor( builder.field(ENABLED_FIELD, enabled) builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) - builder.field(LABELS_FIELD, labels) builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) + builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) builder.field(QUERY_FIELD, query) builder.endObject() @@ -133,12 +137,12 @@ data class PPLMonitor( } out.writeInstant(lastUpdateTime) out.writeOptionalInstant(enabledTime) - out.writeMap(labels) out.writeVInt(triggers.size) triggers.forEach { out.writeEnum(TriggerV2.TriggerV2Type.PPL_TRIGGER) it.writeTo(out) } + out.writeEnum(queryLanguage) out.writeString(query) } @@ -151,17 +155,31 @@ data class PPLMonitor( SCHEDULE_FIELD to schedule, LAST_UPDATE_TIME_FIELD to lastUpdateTime.toEpochMilli(), ENABLED_TIME_FIELD to enabledTime?.toEpochMilli(), - LABELS_FIELD to labels, TRIGGERS_FIELD to triggers, + QUERY_LANGUAGE_FIELD to queryLanguage.value, QUERY_FIELD to query ) } + 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" + 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 @@ -172,10 +190,10 @@ data class PPLMonitor( var monitorType: String = PPL_MONITOR_TYPE var enabled = true var schedule: Schedule? = null - val lastUpdateTime: Instant = Instant.now() // set time of update or first creation as lastUpdateTime + var lastUpdateTime: Instant? = null var enabledTime: Instant? = null - var labels: Map = emptyMap() val triggers: MutableList = mutableListOf() + var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL var query: String? = null @@ -191,7 +209,7 @@ data class PPLMonitor( ENABLED_FIELD -> enabled = xcp.booleanValue() SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) ENABLED_TIME_FIELD -> enabledTime = xcp.instant() - LABELS_FIELD -> labels = xcp.map() + LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() TRIGGERS_FIELD -> { XContentParserUtils.ensureExpectedToken( XContentParser.Token.START_ARRAY, @@ -202,7 +220,15 @@ data class PPLMonitor( triggers.add(PPLTrigger.parseInner(xcp)) } } + QUERY_LANGUAGE_FIELD -> { + val input = xcp.text() + val enumMatchResult = QueryLanguage.enumFromString(input) + ?: throw IllegalArgumentException("Invalid value for $QUERY_LANGUAGE_FIELD: $input. Supported values are ${QueryLanguage.entries.map { it.value }}") + queryLanguage = enumMatchResult + } QUERY_FIELD -> query = xcp.text() + TYPE_FIELD -> continue // TODO: can this field be done away with, it's redundant with outer scheduled job metadata field + else -> throw IllegalArgumentException("Unexpected field \"$fieldName\" when parsing PPL Monitor") } } @@ -228,17 +254,16 @@ data class PPLMonitor( enabledTime = null } - // check if all label key,values are String,String, throw exception otherwise - try { - labels = convertLabelsMap(labels) - } catch (e: ClassCastException) { - throw IllegalArgumentException("invalid maps field, please ensure all labels are strings") - } - // check for required fields requireNotNull(name) { "Monitor name is null" } requireNotNull(schedule) { "Schedule is null" } + requireNotNull(queryLanguage) { "Query language is null" } requireNotNull(query) { "Query is null" } + requireNotNull(lastUpdateTime) { "Last update time is null" } + + if (queryLanguage == QueryLanguage.SQL) { + throw IllegalArgumentException("SQL queries are not supported. Please use a PPL query.") + } /* return PPLMonitor */ return PPLMonitor( @@ -249,8 +274,8 @@ data class PPLMonitor( schedule, lastUpdateTime, enabledTime, - labels, triggers, + queryLanguage, query ) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 79951dba..5acffa69 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -256,6 +256,7 @@ data class PPLTrigger( actions.add(Action.parse(xcp)) } } + else -> throw IllegalArgumentException("Unexpected field $fieldName when parsing PPL Trigger") } } From bd99f0b6ae1fdd358c42d938aaf3ed9af27591a6 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 2 Sep 2025 12:44:56 -0700 Subject: [PATCH 05/30] removing logger from ScheduledJob --- .../org/opensearch/commons/alerting/model/ScheduledJob.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt index b37e037c..cf8417c2 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt @@ -7,9 +7,6 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant -import org.apache.logging.log4j.LogManager - -private val log = LogManager.getLogger(ScheduledJob::class.java) interface ScheduledJob : BaseModel { From 6418e2fb22573ab66a4ee716bdb903911486fe76 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 2 Sep 2025 13:55:57 -0700 Subject: [PATCH 06/30] defaulting lastUpdateTime to now if it's null --- .../kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index ec2ae37f..3c01e144 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -254,6 +254,8 @@ data class PPLMonitor( enabledTime = null } + lastUpdateTime = lastUpdateTime ?: Instant.now() + // check for required fields requireNotNull(name) { "Monitor name is null" } requireNotNull(schedule) { "Schedule is null" } From 146292b479e0116551e80186409847811a2b4b7b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 2 Sep 2025 14:10:26 -0700 Subject: [PATCH 07/30] removing redundant inner scheduled job type field --- .../kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index 3c01e144..d16bddf4 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -101,7 +101,6 @@ data class PPLMonitor( if (params.paramAsBoolean("with_type", false)) { builder.startObject(MONITOR_V2_TYPE) } - builder.field(TYPE_FIELD, MONITOR_V2_TYPE) // this field is ScheduledJob metadata, include despite it not being a class field builder.field(MONITOR_TYPE_FIELD, PPL_MONITOR_TYPE) @@ -227,7 +226,6 @@ data class PPLMonitor( queryLanguage = enumMatchResult } QUERY_FIELD -> query = xcp.text() - TYPE_FIELD -> continue // TODO: can this field be done away with, it's redundant with outer scheduled job metadata field else -> throw IllegalArgumentException("Unexpected field \"$fieldName\" when parsing PPL Monitor") } } From 2acf9e092ea279494bd53f74dbc4e053decd21ed Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 2 Sep 2025 14:25:01 -0700 Subject: [PATCH 08/30] adding null handling to trigger parsing --- .../commons/alerting/model/PPLMonitor.kt | 6 ---- .../commons/alerting/model/PPLTrigger.kt | 34 +++++++++++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index d16bddf4..abb5aa20 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -279,11 +279,5 @@ data class PPLMonitor( query ) } - -// @JvmStatic -// @Throws(IOException::class) -// fun readFrom(sin: StreamInput): PPLMonitor { -// return PPLMonitor(sin) -// } } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 5acffa69..a1ff53da 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -56,9 +56,7 @@ data class PPLTrigger( sin.readOptionalInstant(), // lastTriggeredTime sin.readList(::Action), // actions sin.readEnum(TriggerMode::class.java), // trigger mode - // TODO: add validation to ensure numResultsCondition and numResultsValue or customCondition are non-null based on condition type? sin.readEnum(ConditionType::class.java), // condition type - // TODO: can updated StreamInput be picked up so we can use readOptionalEnum? if (sin.readBoolean()) sin.readEnum(NumResultsCondition::class.java) else null, // num results condition sin.readOptionalLong(), // num results value sin.readOptionalString(), // custom condition @@ -81,7 +79,7 @@ data class PPLTrigger( out.writeEnum(mode) out.writeEnum(conditionType) - out.writeBoolean(numResultsCondition != null) // TODO: look for built-in writeOptionalEnum support + out.writeBoolean(numResultsCondition != null) numResultsCondition?.let { out.writeEnum(numResultsCondition) } out.writeOptionalLong(numResultsValue) @@ -202,8 +200,6 @@ data class PPLTrigger( val fieldName = xcp.currentName() xcp.nextToken() - // TODO: if e.g. trigger is num results but user explicitly passes in custom_condition = null, parse fails because - // TODO: it tries to parse a text value from null when (fieldName) { ID_FIELD -> id = xcp.text() NAME_FIELD -> name = xcp.text() @@ -226,13 +222,29 @@ data class PPLTrigger( conditionType = enumMatchResult } NUM_RESULTS_CONDITION_FIELD -> { - 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 + numResultsCondition = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + 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 }}") + enumMatchResult + } + } + NUM_RESULTS_VALUE_FIELD -> { + numResultsValue = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + xcp.longValue() + } + } + CUSTOM_CONDITION_FIELD -> { + customCondition = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + xcp.text() + } } - NUM_RESULTS_VALUE_FIELD -> numResultsValue = xcp.longValue() - CUSTOM_CONDITION_FIELD -> customCondition = xcp.text() SUPPRESS_FIELD -> { suppressDuration = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { null From 29ae09bc31e6b56b9ca0a0d2b95431dbcbf52989 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 3 Sep 2025 12:41:11 -0700 Subject: [PATCH 09/30] added fields for update monitor in IndexMonitorRequest --- .../alerting/action/IndexMonitorV2Request.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt index 4617aec3..a259ff76 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt @@ -7,41 +7,42 @@ import org.opensearch.action.support.WriteRequest import org.opensearch.commons.alerting.model.MonitorV2 import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput +import org.opensearch.rest.RestRequest class IndexMonitorV2Request : ActionRequest { -// val monitorId: String + val monitorId: String val seqNo: Long val primaryTerm: Long val refreshPolicy: WriteRequest.RefreshPolicy -// val method: RestRequest.Method + val method: RestRequest.Method var monitorV2: MonitorV2 // val rbacRoles: List? constructor( -// monitorId: String, + monitorId: String, seqNo: Long, primaryTerm: Long, refreshPolicy: WriteRequest.RefreshPolicy, -// method: RestRequest.Method, + method: RestRequest.Method, monitorV2: MonitorV2, // rbacRoles: List? = null ) : super() { -// this.monitorId = monitorId + this.monitorId = monitorId this.seqNo = seqNo this.primaryTerm = primaryTerm this.refreshPolicy = refreshPolicy -// this.method = method + this.method = method this.monitorV2 = monitorV2 // this.rbacRoles = rbacRoles } @Throws(IOException::class) constructor(sin: StreamInput) : this( -// monitorId = sin.readString(), + monitorId = sin.readString(), seqNo = sin.readLong(), primaryTerm = sin.readLong(), refreshPolicy = WriteRequest.RefreshPolicy.readFrom(sin), -// method = sin.readEnum(RestRequest.Method::class.java), + method = sin.readEnum(RestRequest.Method::class.java), monitorV2 = MonitorV2.readFrom(sin), // rbacRoles = sin.readOptionalStringList() ) @@ -52,11 +53,11 @@ class IndexMonitorV2Request : ActionRequest { @Throws(IOException::class) override fun writeTo(out: StreamOutput) { -// out.writeString(monitorId) + out.writeString(monitorId) out.writeLong(seqNo) out.writeLong(primaryTerm) refreshPolicy.writeTo(out) -// out.writeEnum(method) + out.writeEnum(method) MonitorV2.writeTo(out, monitorV2) // out.writeOptionalStringCollection(rbacRoles) } From 95b7e8cffe340941244eca1fe3f5eebaa1eabe0c Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 3 Sep 2025 15:30:17 -0700 Subject: [PATCH 10/30] refactored MonitorV2 parse to read outer monitor type field and delegate to correct parse func accordingly --- .../commons/alerting/model/MonitorV2.kt | 34 +++++++++---------- .../commons/alerting/model/PPLMonitor.kt | 17 +++++----- .../commons/alerting/model/PPLTrigger.kt | 7 ++++ .../commons/alerting/model/TriggerV2.kt | 27 ++------------- 4 files changed, 36 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index e4e40fb2..9ecfa9ca 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -6,6 +6,7 @@ import org.opensearch.common.CheckedFunction import org.opensearch.commons.alerting.model.Monitor.Companion import org.opensearch.commons.alerting.model.Monitor.Companion.INPUTS_FIELD import org.opensearch.commons.alerting.model.PPLMonitor.Companion.PPL_MONITOR_TYPE +import org.opensearch.commons.alerting.model.PPLTrigger.Companion.PPL_TRIGGER_FIELD import org.opensearch.commons.alerting.model.Trigger.Type import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION @@ -21,7 +22,6 @@ import org.opensearch.core.xcontent.XContentBuilder import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils -// TODO: maybe make this abstract class? put init block logic here for all monitors? interface MonitorV2 : ScheduledJob { override val id: String override val version: Long @@ -50,7 +50,6 @@ interface MonitorV2 : ScheduledJob { companion object { // scheduled job field names - const val TYPE_FIELD = "type" const val MONITOR_V2_TYPE = "monitor_v2" // scheduled job type is MonitorV2 // field names @@ -76,25 +75,26 @@ interface MonitorV2 : ScheduledJob { @JvmOverloads @Throws(IOException::class) fun parse(xcp: XContentParser): MonitorV2 { - /* - TODO: this default implementation is short-term and inextensible - a correct implementation should 1) scan for monitor type field - 2) delegate to the parse function of the MonitorV2 implementation, - just like how TriggerV2 interface does it. - The problem is the (internal) monitor type field is at the same - level as all the other monitor fields, which means we would need some - way of parsing the same XContent twice - possible work around: require monitor type to be very first field - if first monitor type field is absent, assume ppl monitor as default - */ - return PPLMonitor.parse(xcp) + /* parse outer object for monitorV2 type, then delegate to correct monitorV2 parser */ + + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) // outer monitor object start + + 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") + + 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 { - val monitorType = sin.readEnum(MonitorV2Type::class.java) - return when (monitorType) { + return when (val monitorType = sin.readEnum(MonitorV2Type::class.java)) { MonitorV2Type.PPL_MONITOR -> PPLMonitor(sin) - else -> throw IllegalStateException("Unexpected input [$monitorType] when reading MonitorV2") + else -> throw IllegalStateException("Unexpected input \"$monitorType\" when reading MonitorV2") } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index abb5aa20..079c95f7 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -13,10 +13,6 @@ import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_ID import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_VERSION import org.opensearch.commons.alerting.model.MonitorV2.Companion.SCHEDULE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.TRIGGERS_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.TYPE_FIELD -import org.opensearch.commons.alerting.model.PPLTrigger.Companion.NUM_RESULTS_CONDITION_FIELD -import org.opensearch.commons.alerting.model.PPLTrigger.NumResultsCondition -import org.opensearch.commons.alerting.model.PPLTrigger.TriggerMode import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION import org.opensearch.commons.alerting.util.instant @@ -94,7 +90,7 @@ data class PPLMonitor( ) override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { - builder.startObject() + builder.startObject() // overall start object // if this is being written as ScheduledJob, add extra object layer and add ScheduledJob // related metadata, default to false @@ -102,8 +98,10 @@ data class PPLMonitor( builder.startObject(MONITOR_V2_TYPE) } - // this field is ScheduledJob metadata, include despite it not being a class field - builder.field(MONITOR_TYPE_FIELD, PPL_MONITOR_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) @@ -113,13 +111,16 @@ data class PPLMonitor( builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) builder.field(QUERY_FIELD, query) - builder.endObject() + + 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 } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index a1ff53da..5bb92fa1 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -193,7 +193,14 @@ data class PPLTrigger( /* parse */ XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) // outer trigger object start + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) // ppl_trigger field name + val triggerType = xcp.currentName() + if (triggerType != PPL_TRIGGER_FIELD) { + throw IllegalStateException("when parsing PPLMonitor, expected trigger to be of type $PPL_TRIGGER_FIELD " + + "but instead got \"$triggerType\"") + } + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner trigger object start while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt index d29e3fba..99cb8e14 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -13,8 +13,8 @@ interface TriggerV2 : BaseModel { val id: String val name: String val severity: Severity - val suppressDuration: TimeValue? // TODO: move to MonitorV2 definition - val expireDuration: TimeValue? // TODO: move to MonitorV2 definition + val suppressDuration: TimeValue? + val expireDuration: TimeValue? var lastTriggeredTime: Instant? val actions: List @@ -49,33 +49,12 @@ interface TriggerV2 : BaseModel { const val EXPIRE_FIELD = "expires" const val ACTIONS_FIELD = "actions" -// @Throws(IOException::class) -// fun parse(xcp: XContentParser): TriggerV2 { -// // TODO: dead code until a MonitorV2 interface level parse() that delegates by monitor type is implemented -// val trigger: TriggerV2 -// -// val triggerV2TypeNames = TriggerV2Type.entries.map { it.value } -// -// XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) -// XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) -// -// if (!triggerV2TypeNames.contains(xcp.currentName())) { -// throw IllegalArgumentException("Invalid trigger type ${xcp.currentName()}") -// } -// -// XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) -// trigger = xcp.namedObject(TriggerV2::class.java, xcp.currentName(), null) -// XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, xcp.nextToken(), xcp) -// -// return trigger -// } - @JvmStatic @Throws(IOException::class) fun readFrom(sin: StreamInput): TriggerV2 { return when (val type = sin.readEnum(TriggerV2Type::class.java)) { TriggerV2Type.PPL_TRIGGER -> PPLTrigger(sin) - else -> throw IllegalStateException("Unexpected input [$type] when reading TriggerV2") + else -> throw IllegalStateException("Unexpected input \"$type\" when reading TriggerV2") } } } From 2154cbe6152efd02886686bafadeb600f143fb68 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 4 Sep 2025 10:18:41 -0700 Subject: [PATCH 11/30] linter issues --- .../ppl/action/TransportPPLQueryRequest.java | 31 ++++++++++--------- .../ppl/action/TransportPPLQueryResponse.java | 18 +++++------ .../commons/ppl/format/ErrorFormatter.java | 26 ++++++---------- .../opensearch/commons/ppl/format/Format.java | 10 +++--- .../ppl/format/JsonResponseFormatter.java | 8 ++--- .../commons/ppl/serde/DataSourceType.java | 4 +-- .../commons/ppl/serde/SerializeUtils.java | 18 +++++------ .../commons/ppl/util/PPLQueryRequest.java | 20 +++++++----- .../ppl/util/PPLQueryRequestFactory.java | 29 ++++++++--------- .../alerting/action/DeleteMonitorV2Request.kt | 4 +-- .../action/DeleteMonitorV2Response.kt | 2 +- .../alerting/action/GetMonitorV2Request.kt | 4 +-- .../alerting/action/GetMonitorV2Response.kt | 4 +-- .../alerting/action/IndexMonitorV2Request.kt | 8 ++--- .../alerting/action/IndexMonitorV2Response.kt | 5 ++- .../alerting/action/SearchMonitorV2Request.kt | 4 +-- .../commons/alerting/model/AlertV2.kt | 12 +++---- .../commons/alerting/model/MonitorV2.kt | 18 ++--------- .../alerting/model/MonitorV2RunResult.kt | 4 +-- .../commons/alerting/model/PPLMonitor.kt | 5 ++- .../alerting/model/PPLMonitorRunResult.kt | 6 ++-- .../commons/alerting/model/PPLTrigger.kt | 18 +++++------ .../alerting/model/PPLTriggerRunResult.kt | 6 ++-- .../commons/alerting/model/TriggerV2.kt | 6 ++-- .../alerting/model/TriggerV2RunResult.kt | 7 ++--- .../commons/alerting/util/IndexUtils.kt | 1 - .../commons/ppl/PPLPluginInterface.kt | 3 +- 27 files changed, 128 insertions(+), 153 deletions(-) diff --git a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java index 5b515547..ddddcfcd 100644 --- a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java +++ b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java @@ -10,10 +10,7 @@ import java.io.IOException; import java.util.Locale; import java.util.Optional; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; + import org.json.JSONObject; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; @@ -25,17 +22,25 @@ import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + //private val log = LogManager.getLogger(TransportExecuteMonitorAction::class.java) @RequiredArgsConstructor public class TransportPPLQueryRequest extends ActionRequest { public static final TransportPPLQueryRequest NULL = new TransportPPLQueryRequest("", null, ""); private final String pplQuery; - @Getter private final JSONObject jsonContent; + @Getter + private final JSONObject jsonContent; - @Getter private final String path; + @Getter + private final String path; - @Getter private String format = ""; + @Getter + private String format = ""; @Setter @Getter @@ -75,16 +80,13 @@ public static TransportPPLQueryRequest fromActionRequest(final ActionRequest act return (TransportPPLQueryRequest) actionRequest; } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { actionRequest.writeTo(osso); - try (InputStreamStreamInput input = - new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { + try (InputStreamStreamInput input = new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { return new TransportPPLQueryRequest(input); } } catch (IOException e) { - throw new IllegalArgumentException( - "failed to parse ActionRequest into TransportPPLQueryRequest", e); + throw new IllegalArgumentException("failed to parse ActionRequest into TransportPPLQueryRequest", e); } } @@ -118,8 +120,7 @@ public Format format() { if (optionalFormat.isPresent()) { return optionalFormat.get(); } else { - throw new IllegalArgumentException( - String.format(Locale.ROOT, "response in %s format is not supported.", format)); + throw new IllegalArgumentException(String.format(Locale.ROOT, "response in %s format is not supported.", format)); } } diff --git a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java index 469a7212..7cd13194 100644 --- a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java +++ b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java @@ -9,17 +9,20 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; -import lombok.Getter; -import lombok.RequiredArgsConstructor; + import org.opensearch.core.action.ActionResponse; import org.opensearch.core.common.io.stream.InputStreamStreamInput; import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + @RequiredArgsConstructor public class TransportPPLQueryResponse extends ActionResponse { - @Getter public final String result; + @Getter + public final String result; public TransportPPLQueryResponse(StreamInput in) throws IOException { super(in); @@ -36,16 +39,13 @@ public static TransportPPLQueryResponse fromActionResponse(ActionResponse action return (TransportPPLQueryResponse) actionResponse; } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { actionResponse.writeTo(osso); - try (StreamInput input = - new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { + try (StreamInput input = new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { return new TransportPPLQueryResponse(input); } } catch (IOException e) { - throw new UncheckedIOException( - "failed to parse ActionResponse into TransportPPLQueryResponse", e); + throw new UncheckedIOException("failed to parse ActionResponse into TransportPPLQueryResponse", e); } } } diff --git a/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java index 2e54ed8f..3286ef1f 100644 --- a/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java +++ b/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java @@ -5,29 +5,24 @@ package org.opensearch.commons.ppl.format; -import com.google.gson.Gson; import java.security.AccessController; import java.security.PrivilegedAction; + +import org.opensearch.commons.ppl.serde.SerializeUtils; + +import com.google.gson.Gson; + import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.experimental.UtilityClass; -import org.opensearch.commons.ppl.serde.SerializeUtils; @UtilityClass public class ErrorFormatter { - private static final Gson PRETTY_PRINT_GSON = - AccessController.doPrivileged( - (PrivilegedAction) - () -> - SerializeUtils.getGsonBuilder() - .setPrettyPrinting() - .disableHtmlEscaping() - .create()); - private static final Gson GSON = - AccessController.doPrivileged( - (PrivilegedAction) - () -> SerializeUtils.getGsonBuilder().disableHtmlEscaping().create()); + private static final Gson PRETTY_PRINT_GSON = AccessController + .doPrivileged((PrivilegedAction) () -> SerializeUtils.getGsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()); + private static final Gson GSON = AccessController + .doPrivileged((PrivilegedAction) () -> SerializeUtils.getGsonBuilder().disableHtmlEscaping().create()); /** Util method to format {@link Throwable} response to JSON string in compact printing. */ public static String compactFormat(Throwable t) { @@ -46,8 +41,7 @@ public static String compactJsonify(Object jsonObject) { } public static String prettyJsonify(Object jsonObject) { - return AccessController.doPrivileged( - (PrivilegedAction) () -> PRETTY_PRINT_GSON.toJson(jsonObject)); + return AccessController.doPrivileged((PrivilegedAction) () -> PRETTY_PRINT_GSON.toJson(jsonObject)); } @RequiredArgsConstructor diff --git a/src/main/java/org/opensearch/commons/ppl/format/Format.java b/src/main/java/org/opensearch/commons/ppl/format/Format.java index 957ea22e..c6d48229 100644 --- a/src/main/java/org/opensearch/commons/ppl/format/Format.java +++ b/src/main/java/org/opensearch/commons/ppl/format/Format.java @@ -5,12 +5,13 @@ package org.opensearch.commons.ppl.format; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; - import java.util.Locale; import java.util.Map; import java.util.Optional; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -26,7 +27,8 @@ public enum Format { EXTENDED("extended"), COST("cost"); - @Getter private final String formatName; + @Getter + private final String formatName; public static final Map RESPONSE_FORMATS; diff --git a/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java index 9abc54e0..7021f2ed 100644 --- a/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java +++ b/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java @@ -42,8 +42,7 @@ public String format(R response) { @Override public String format(Throwable t) { - return AccessController.doPrivileged( - (PrivilegedAction) () -> (style == PRETTY) ? prettyFormat(t) : compactFormat(t)); + return AccessController.doPrivileged((PrivilegedAction) () -> (style == PRETTY) ? prettyFormat(t) : compactFormat(t)); } public String contentType() { @@ -59,8 +58,7 @@ public String contentType() { protected abstract Object buildJsonObject(R response); protected String jsonify(Object jsonObject) { - return AccessController.doPrivileged( - (PrivilegedAction) - () -> (style == PRETTY) ? prettyJsonify(jsonObject) : compactJsonify(jsonObject)); + return AccessController + .doPrivileged((PrivilegedAction) () -> (style == PRETTY) ? prettyJsonify(jsonObject) : compactJsonify(jsonObject)); } } diff --git a/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java b/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java index b8f32758..776f02fa 100644 --- a/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java +++ b/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; + import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -44,8 +45,7 @@ public static void register(DataSourceType... dataSourceTypes) { if (!knownValues.containsKey(upperCaseName)) { knownValues.put(type.name().toUpperCase(Locale.ROOT), type); } else { - throw new IllegalArgumentException( - "DataSourceType with name " + type.name() + " already exists"); + throw new IllegalArgumentException("DataSourceType with name " + type.name() + " already exists"); } } } diff --git a/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java b/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java index 9c0f72b3..fb992f59 100644 --- a/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java +++ b/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java @@ -5,6 +5,8 @@ package org.opensearch.commons.ppl.serde; +import java.lang.reflect.Type; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; @@ -14,34 +16,30 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; -import java.lang.reflect.Type; + import lombok.experimental.UtilityClass; @UtilityClass public class SerializeUtils { private static class DataSourceTypeSerializer implements JsonSerializer { @Override - public JsonElement serialize( - DataSourceType dataSourceType, - Type type, - JsonSerializationContext jsonSerializationContext) { + public JsonElement serialize(DataSourceType dataSourceType, Type type, JsonSerializationContext jsonSerializationContext) { return new JsonPrimitive(dataSourceType.name()); } } private static class DataSourceTypeDeserializer implements JsonDeserializer { @Override - public DataSourceType deserialize( - JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) - throws JsonParseException { + public DataSourceType deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) + throws JsonParseException { return DataSourceType.fromString(jsonElement.getAsString()); } } public static GsonBuilder getGsonBuilder() { return new GsonBuilder() - .registerTypeAdapter(DataSourceType.class, new DataSourceTypeSerializer()) - .registerTypeAdapter(DataSourceType.class, new DataSourceTypeDeserializer()); + .registerTypeAdapter(DataSourceType.class, new DataSourceTypeSerializer()) + .registerTypeAdapter(DataSourceType.class, new DataSourceTypeDeserializer()); } public static Gson buildGson() { diff --git a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java index ab0e635b..14305ac3 100644 --- a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java +++ b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java @@ -7,13 +7,15 @@ import java.util.Locale; import java.util.Optional; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; + import org.json.JSONObject; import org.opensearch.commons.ppl.format.Format; import org.opensearch.commons.ppl.format.JsonResponseFormatter; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + public class PPLQueryRequest { private static final String DEFAULT_PPL_PATH = "/_plugins/_ppl"; @@ -21,9 +23,12 @@ public class PPLQueryRequest { public static final PPLQueryRequest NULL = new PPLQueryRequest("", null, DEFAULT_PPL_PATH, ""); private final String pplQuery; - @Getter private final JSONObject jsonContent; - @Getter private final String path; - @Getter private String format = ""; + @Getter + private final JSONObject jsonContent; + @Getter + private final String path; + @Getter + private String format = ""; @Setter @Getter @@ -66,8 +71,7 @@ public Format format() { if (optionalFormat.isPresent()) { return optionalFormat.get(); } else { - throw new IllegalArgumentException( - String.format(Locale.ROOT, "response in %s format is not supported.", format)); + throw new IllegalArgumentException(String.format(Locale.ROOT, "response in %s format is not supported.", format)); } } } diff --git a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java index 402f6adb..afce77ea 100644 --- a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java +++ b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java @@ -8,6 +8,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; + import org.json.JSONException; import org.json.JSONObject; import org.opensearch.commons.ppl.format.Format; @@ -37,8 +38,7 @@ public static PPLQueryRequest getPPLRequest(RestRequest request) { case POST: return parsePPLRequestFromPayload(request); default: - throw new IllegalArgumentException( - "OpenSearch PPL doesn't supported HTTP " + request.method().name()); + throw new IllegalArgumentException("OpenSearch PPL doesn't supported HTTP " + request.method().name()); } } @@ -59,12 +59,12 @@ private static PPLQueryRequest parsePPLRequestFromPayload(RestRequest restReques boolean pretty = getPrettyOption(restRequest.params()); try { jsonContent = new JSONObject(content); - PPLQueryRequest pplRequest = - new PPLQueryRequest( - jsonContent.getString(PPL_FIELD_NAME), - jsonContent, - restRequest.path(), - format.getFormatName()); + PPLQueryRequest pplRequest = new PPLQueryRequest( + jsonContent.getString(PPL_FIELD_NAME), + jsonContent, + restRequest.path(), + format.getFormatName() + ); // set sanitize option if csv format if (format.equals(Format.CSV)) { pplRequest.sanitize(getSanitizeOption(restRequest.params())); @@ -80,17 +80,14 @@ private static PPLQueryRequest parsePPLRequestFromPayload(RestRequest restReques } private static Format getFormat(Map requestParams, String path) { - String formatName = - requestParams.containsKey(QUERY_PARAMS_FORMAT) - ? requestParams.get(QUERY_PARAMS_FORMAT).toLowerCase(Locale.ROOT) - : isExplainRequest(path) ? DEFAULT_EXPLAIN_FORMAT : DEFAULT_RESPONSE_FORMAT; - Optional optionalFormat = - isExplainRequest(path) ? Format.ofExplain(formatName) : Format.of(formatName); + String formatName = requestParams.containsKey(QUERY_PARAMS_FORMAT) ? requestParams.get(QUERY_PARAMS_FORMAT).toLowerCase(Locale.ROOT) + : isExplainRequest(path) ? DEFAULT_EXPLAIN_FORMAT + : DEFAULT_RESPONSE_FORMAT; + Optional optionalFormat = isExplainRequest(path) ? Format.ofExplain(formatName) : Format.of(formatName); if (optionalFormat.isPresent()) { return optionalFormat.get(); } else { - throw new IllegalArgumentException( - "Failed to create executor due to unknown response format: " + formatName); + throw new IllegalArgumentException("Failed to create executor due to unknown response format: " + formatName); } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt index dee79394..410ae250 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt @@ -1,11 +1,11 @@ package org.opensearch.commons.alerting.action -import java.io.IOException import org.opensearch.action.ActionRequest import org.opensearch.action.ActionRequestValidationException import org.opensearch.action.support.WriteRequest import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput +import java.io.IOException class DeleteMonitorV2Request : ActionRequest { val monitorV2Id: String @@ -31,4 +31,4 @@ class DeleteMonitorV2Request : ActionRequest { out.writeString(monitorV2Id) refreshPolicy.writeTo(out) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt index 228a561e..6d7ffef0 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt @@ -35,4 +35,4 @@ class DeleteMonitorV2Response : BaseResponse { .field(IndexUtils._VERSION, version) .endObject() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt index 42977871..bd944a50 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt @@ -1,11 +1,11 @@ package org.opensearch.commons.alerting.action -import java.io.IOException import org.opensearch.action.ActionRequest import org.opensearch.action.ActionRequestValidationException import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput import org.opensearch.search.fetch.subphase.FetchSourceContext +import java.io.IOException class GetMonitorV2Request : ActionRequest { val monitorV2Id: String @@ -44,4 +44,4 @@ class GetMonitorV2Request : ActionRequest { out.writeBoolean(srcContext != null) srcContext?.writeTo(out) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt index 72a7eb8b..8232fda0 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt @@ -1,6 +1,5 @@ package org.opensearch.commons.alerting.action -import java.io.IOException import org.opensearch.commons.alerting.model.MonitorV2 import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._PRIMARY_TERM @@ -11,6 +10,7 @@ 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 class GetMonitorV2Response : BaseResponse { var id: String @@ -72,4 +72,4 @@ class GetMonitorV2Response : BaseResponse { } return builder.endObject() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt index a259ff76..298372b7 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt @@ -1,6 +1,5 @@ package org.opensearch.commons.alerting.action -import java.io.IOException import org.opensearch.action.ActionRequest import org.opensearch.action.ActionRequestValidationException import org.opensearch.action.support.WriteRequest @@ -8,6 +7,7 @@ import org.opensearch.commons.alerting.model.MonitorV2 import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput import org.opensearch.rest.RestRequest +import java.io.IOException class IndexMonitorV2Request : ActionRequest { val monitorId: String @@ -24,7 +24,7 @@ class IndexMonitorV2Request : ActionRequest { primaryTerm: Long, refreshPolicy: WriteRequest.RefreshPolicy, method: RestRequest.Method, - monitorV2: MonitorV2, + monitorV2: MonitorV2 // rbacRoles: List? = null ) : super() { this.monitorId = monitorId @@ -43,7 +43,7 @@ class IndexMonitorV2Request : ActionRequest { primaryTerm = sin.readLong(), refreshPolicy = WriteRequest.RefreshPolicy.readFrom(sin), method = sin.readEnum(RestRequest.Method::class.java), - monitorV2 = MonitorV2.readFrom(sin), + monitorV2 = MonitorV2.readFrom(sin) // rbacRoles = sin.readOptionalStringList() ) @@ -61,4 +61,4 @@ class IndexMonitorV2Request : ActionRequest { MonitorV2.writeTo(out, monitorV2) // out.writeOptionalStringCollection(rbacRoles) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt index 9d481325..c47f12ab 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt @@ -1,7 +1,5 @@ package org.opensearch.commons.alerting.action -import java.io.IOException -import org.opensearch.commons.alerting.model.Monitor import org.opensearch.commons.alerting.model.MonitorV2 import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._PRIMARY_TERM @@ -12,6 +10,7 @@ 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 class IndexMonitorV2Response : BaseResponse { var id: String @@ -66,4 +65,4 @@ class IndexMonitorV2Response : BaseResponse { companion object { const val MONITOR_V2_FIELD = "monitor_v2" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt index 32cbf48d..12a2129f 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt @@ -1,11 +1,11 @@ package org.opensearch.commons.alerting.action -import java.io.IOException import org.opensearch.action.ActionRequest import org.opensearch.action.ActionRequestValidationException import org.opensearch.action.search.SearchRequest import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput +import java.io.IOException class SearchMonitorV2Request : ActionRequest { val searchRequest: SearchRequest @@ -29,4 +29,4 @@ class SearchMonitorV2Request : ActionRequest { override fun writeTo(out: StreamOutput) { searchRequest.writeTo(out) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index b3e5d620..935f4ddd 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -1,7 +1,5 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.opensearch.common.lucene.uid.Versions import org.opensearch.commons.alerting.alerts.AlertError import org.opensearch.commons.alerting.model.Alert.Companion.ACKNOWLEDGED_TIME_FIELD @@ -35,6 +33,8 @@ 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 data class AlertV2( val id: String = NO_ID, @@ -56,7 +56,7 @@ data class AlertV2( val errorHistory: List, val severity: String, val actionExecutionResults: List, - val executionId: String? = null, + val executionId: String? = null ) : Writeable, ToXContent { @Throws(IOException::class) constructor(sin: StreamInput) : this( @@ -83,7 +83,7 @@ data class AlertV2( errorHistory = sin.readList(::AlertError), severity = sin.readString(), actionExecutionResults = sin.readList(::ActionExecutionResult), - executionId = sin.readOptionalString(), + executionId = sin.readOptionalString() ) @Throws(IOException::class) @@ -153,7 +153,7 @@ data class AlertV2( EXPIRATION_TIME_FIELD to expirationTime?.toEpochMilli(), SEVERITY_FIELD to severity, START_TIME_FIELD to startTime.toEpochMilli(), - STATE_FIELD to state.toString(), + STATE_FIELD to state.toString() ) } @@ -245,7 +245,7 @@ data class AlertV2( errorHistory = errorHistory, severity = severity, actionExecutionResults = actionExecutionResults, - executionId = executionId, + executionId = executionId ) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index 9ecfa9ca..7ca5d3ab 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -1,26 +1,15 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.opensearch.common.CheckedFunction -import org.opensearch.commons.alerting.model.Monitor.Companion -import org.opensearch.commons.alerting.model.Monitor.Companion.INPUTS_FIELD import org.opensearch.commons.alerting.model.PPLMonitor.Companion.PPL_MONITOR_TYPE -import org.opensearch.commons.alerting.model.PPLTrigger.Companion.PPL_TRIGGER_FIELD -import org.opensearch.commons.alerting.model.Trigger.Type -import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID -import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION -import org.opensearch.commons.alerting.util.nonOptionalTimeField -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.XContent -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 @@ -72,7 +61,6 @@ interface MonitorV2 : ScheduledJob { ) @JvmStatic - @JvmOverloads @Throws(IOException::class) fun parse(xcp: XContentParser): MonitorV2 { /* parse outer object for monitorV2 type, then delegate to correct monitorV2 parser */ @@ -107,4 +95,4 @@ interface MonitorV2 : ScheduledJob { } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt index 12e855b9..be21feca 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt @@ -1,11 +1,11 @@ package org.opensearch.commons.alerting.model -import java.time.Instant import org.opensearch.commons.alerting.alerts.AlertError 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 java.time.Instant interface MonitorV2RunResult : Writeable, ToXContent { val monitorName: String @@ -66,4 +66,4 @@ interface MonitorV2RunResult : Writeab return map as Map } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index 079c95f7..d9db5e3c 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -1,7 +1,5 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.apache.logging.log4j.LogManager import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD @@ -24,6 +22,8 @@ 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 private val logger = LogManager.getLogger(PPLMonitor::class.java) @@ -196,7 +196,6 @@ data class PPLMonitor( 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) { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt index 9b2b6e8e..0cbcea0d 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt @@ -1,7 +1,5 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.opensearch.commons.alerting.alerts.AlertError import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.ERROR_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.MONITOR_NAME_FIELD @@ -14,6 +12,8 @@ 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 +import java.time.Instant data class PPLMonitorRunResult( override val monitorName: String, @@ -69,4 +69,4 @@ data class PPLMonitorRunResult( companion object { const val PPL_QUERY_RESULTS_FIELD = "ppl_query_results" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 5bb92fa1..540ca9bc 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -1,7 +1,5 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.apache.logging.log4j.LogManager import org.opensearch.common.CheckedFunction import org.opensearch.common.UUIDs @@ -17,7 +15,6 @@ import org.opensearch.commons.alerting.model.TriggerV2.Severity 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.commons.authuser.User import org.opensearch.core.ParseField import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput @@ -26,6 +23,8 @@ 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 private val logger = LogManager.getLogger(PPLTrigger::class.java) @@ -59,7 +58,7 @@ data class PPLTrigger( 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 + sin.readOptionalString() // custom condition ) @Throws(IOException::class) @@ -118,7 +117,7 @@ data class PPLTrigger( CONDITION_TYPE_FIELD to conditionType.value, NUM_RESULTS_CONDITION_FIELD to numResultsCondition?.value, NUM_RESULTS_VALUE_FIELD to numResultsValue, - CUSTOM_CONDITION_FIELD to customCondition, + CUSTOM_CONDITION_FIELD to customCondition ) } @@ -174,7 +173,6 @@ data class PPLTrigger( CheckedFunction { parseInner(it) } ) - @JvmStatic @Throws(IOException::class) fun parseInner(xcp: XContentParser): PPLTrigger { @@ -197,8 +195,10 @@ data class PPLTrigger( XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) // ppl_trigger field name val triggerType = xcp.currentName() if (triggerType != PPL_TRIGGER_FIELD) { - throw IllegalStateException("when parsing PPLMonitor, expected trigger to be of type $PPL_TRIGGER_FIELD " + - "but instead got \"$triggerType\"") + throw IllegalStateException( + "when parsing PPLMonitor, expected trigger to be of type $PPL_TRIGGER_FIELD " + + "but instead got \"$triggerType\"" + ) } XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner trigger object start @@ -313,7 +313,7 @@ data class PPLTrigger( conditionType, numResultsCondition, numResultsValue, - customCondition, + customCondition ) } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt index 7de608ca..5d46dcf7 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt @@ -1,7 +1,5 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.opensearch.commons.alerting.alerts.AlertError import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.ERROR_FIELD import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.NAME_FIELD @@ -10,6 +8,8 @@ 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 +import java.time.Instant data class PPLTriggerRunResult( override var triggerName: String, @@ -66,4 +66,4 @@ data class PPLTriggerRunResult( return QueryLevelTriggerRunResult(sin) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt index 99cb8e14..f8910622 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -1,12 +1,12 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.opensearch.common.unit.TimeValue import org.opensearch.commons.alerting.model.PPLTrigger.Companion.PPL_TRIGGER_FIELD import org.opensearch.commons.alerting.model.action.Action import org.opensearch.commons.notifications.model.BaseModel import org.opensearch.core.common.io.stream.StreamInput +import java.io.IOException +import java.time.Instant interface TriggerV2 : BaseModel { @@ -58,4 +58,4 @@ interface TriggerV2 : BaseModel { } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt index 95a4bc40..d70809db 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt @@ -1,12 +1,9 @@ package org.opensearch.commons.alerting.model -import java.io.IOException -import java.time.Instant import org.opensearch.commons.alerting.alerts.AlertError -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 java.time.Instant interface TriggerV2RunResult : Writeable, ToXContent { @@ -27,4 +24,4 @@ interface TriggerV2RunResult : Writeable, ToXContent { const val TRIGGERED_FIELD = "triggered" const val ERROR_FIELD = "error" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt b/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt index ce82b19d..0ea1d3cc 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/util/IndexUtils.kt @@ -67,7 +67,6 @@ fun XContentBuilder.optionalUsernameField(name: String, user: User?): XContentBu } fun XContentBuilder.nonOptionalTimeField(name: String, instant: Instant): XContentBuilder { - // second name as readableName should be different than first name return this.timeField(name, "${name}_in_millis", instant.toEpochMilli()) } diff --git a/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt index ff93fbdf..2b7a2709 100644 --- a/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt +++ b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt @@ -1,6 +1,5 @@ package org.opensearch.commons.ppl -import org.opensearch.commons.notifications.action.BaseResponse import org.opensearch.commons.ppl.action.PPLQueryAction import org.opensearch.commons.ppl.action.TransportPPLQueryRequest import org.opensearch.commons.ppl.action.TransportPPLQueryResponse @@ -48,4 +47,4 @@ object PPLPluginInterface { } } as ActionListener } -} \ No newline at end of file +} From 14f34a0b5b807f30ce14abbe1fe3072b36aebc13 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 4 Sep 2025 11:51:28 -0700 Subject: [PATCH 12/30] adding schemaVersion --- .../opensearch/commons/alerting/model/MonitorV2.kt | 2 ++ .../opensearch/commons/alerting/model/PPLMonitor.kt | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index 7ca5d3ab..1c4f1974 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -10,6 +10,7 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant +import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION interface MonitorV2 : ScheduledJob { override val id: String @@ -19,6 +20,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 schemaVersion: Int // for updating monitors val triggers: List fun asTemplateArg(): Map diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index d9db5e3c..776be01a 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -24,6 +24,8 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant +import org.opensearch.commons.alerting.model.Monitor.Companion.SCHEMA_VERSION_FIELD +import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION private val logger = LogManager.getLogger(PPLMonitor::class.java) @@ -40,6 +42,7 @@ data class PPLMonitor( override val schedule: Schedule, override val lastUpdateTime: Instant, override val enabledTime: Instant?, + override val schemaVersion: Int = NO_SCHEMA_VERSION, override val triggers: List, val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported val query: String @@ -84,6 +87,7 @@ data class PPLMonitor( schedule = Schedule.readFrom(sin), lastUpdateTime = sin.readInstant(), enabledTime = sin.readOptionalInstant(), + schemaVersion = sin.readInt(), triggers = sin.readList(TriggerV2::readFrom), queryLanguage = sin.readEnum(QueryLanguage::class.java), query = sin.readString() @@ -106,8 +110,9 @@ data class PPLMonitor( builder.field(NAME_FIELD, name) builder.field(SCHEDULE_FIELD, schedule) builder.field(ENABLED_FIELD, enabled) - builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) + builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) + builder.field(SCHEMA_VERSION_FIELD, schemaVersion) builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) builder.field(QUERY_FIELD, query) @@ -137,6 +142,7 @@ data class PPLMonitor( } out.writeInstant(lastUpdateTime) out.writeOptionalInstant(enabledTime) + out.writeInt(schemaVersion) out.writeVInt(triggers.size) triggers.forEach { out.writeEnum(TriggerV2.TriggerV2Type.PPL_TRIGGER) @@ -192,6 +198,7 @@ data class PPLMonitor( var schedule: Schedule? = null var lastUpdateTime: Instant? = null var enabledTime: Instant? = null + var schemaVersion = NO_SCHEMA_VERSION val triggers: MutableList = mutableListOf() var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL var query: String? = null @@ -207,8 +214,9 @@ data class PPLMonitor( MONITOR_TYPE_FIELD -> monitorType = xcp.text() ENABLED_FIELD -> enabled = xcp.booleanValue() SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) - ENABLED_TIME_FIELD -> enabledTime = xcp.instant() LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() + ENABLED_TIME_FIELD -> enabledTime = xcp.instant() + SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() TRIGGERS_FIELD -> { XContentParserUtils.ensureExpectedToken( XContentParser.Token.START_ARRAY, @@ -274,6 +282,7 @@ data class PPLMonitor( schedule, lastUpdateTime, enabledTime, + schemaVersion, triggers, queryLanguage, query From 0b066ec7b3c46b91ac66ef9728ef009db01862de Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 4 Sep 2025 13:03:14 -0700 Subject: [PATCH 13/30] linter issues --- .../kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt | 1 - .../org/opensearch/commons/alerting/model/PPLMonitor.kt | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index 1c4f1974..20d6b437 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -10,7 +10,6 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant -import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION interface MonitorV2 : ScheduledJob { override val id: String diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index 776be01a..de2a39b4 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -1,6 +1,7 @@ package org.opensearch.commons.alerting.model import org.apache.logging.log4j.LogManager +import org.opensearch.commons.alerting.model.Monitor.Companion.SCHEMA_VERSION_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD @@ -11,6 +12,7 @@ import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_ID import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_VERSION import org.opensearch.commons.alerting.model.MonitorV2.Companion.SCHEDULE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.TRIGGERS_FIELD +import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION import org.opensearch.commons.alerting.util.instant @@ -24,8 +26,6 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant -import org.opensearch.commons.alerting.model.Monitor.Companion.SCHEMA_VERSION_FIELD -import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION private val logger = LogManager.getLogger(PPLMonitor::class.java) From affa2acecb1ee2e4a442b068ac7191ef3d234379 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 4 Sep 2025 13:06:04 -0700 Subject: [PATCH 14/30] initial lookback window implementation --- .../commons/alerting/model/MonitorV2.kt | 5 ++- .../commons/alerting/model/PPLMonitor.kt | 43 ++++++++++++++++--- .../commons/alerting/model/PPLTrigger.kt | 2 + 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt index 20d6b437..489e1624 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt @@ -1,6 +1,7 @@ package org.opensearch.commons.alerting.model import org.opensearch.common.CheckedFunction +import org.opensearch.common.unit.TimeValue import org.opensearch.commons.alerting.model.PPLMonitor.Companion.PPL_MONITOR_TYPE import org.opensearch.core.ParseField import org.opensearch.core.common.io.stream.StreamInput @@ -19,8 +20,9 @@ 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 schemaVersion: Int // for updating monitors val triggers: List + val schemaVersion: Int // for updating monitors + val lookBackWindow: TimeValue? // how far back to look when querying data during monitor execution fun asTemplateArg(): Map @@ -50,6 +52,7 @@ interface MonitorV2 : ScheduledJob { const val LAST_UPDATE_TIME_FIELD = "last_update_time" const val ENABLED_TIME_FIELD = "enabled_time" const val TRIGGERS_FIELD = "triggers" + const val LOOK_BACK_WINDOW_FIELD = "look_back_window" // default values const val NO_ID = "" diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index de2a39b4..fff5b4bb 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -1,10 +1,12 @@ package org.opensearch.commons.alerting.model import org.apache.logging.log4j.LogManager +import org.opensearch.common.unit.TimeValue import org.opensearch.commons.alerting.model.Monitor.Companion.SCHEMA_VERSION_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD +import org.opensearch.commons.alerting.model.MonitorV2.Companion.LOOK_BACK_WINDOW_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_TYPE_FIELD import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_V2_TYPE import org.opensearch.commons.alerting.model.MonitorV2.Companion.NAME_FIELD @@ -42,8 +44,9 @@ data class PPLMonitor( override val schedule: Schedule, override val lastUpdateTime: Instant, override val enabledTime: Instant?, - override val schemaVersion: Int = NO_SCHEMA_VERSION, override val triggers: List, + override val schemaVersion: Int = NO_SCHEMA_VERSION, + override val lookBackWindow: TimeValue? = null, val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported val query: String ) : MonitorV2 { @@ -87,8 +90,9 @@ data class PPLMonitor( schedule = Schedule.readFrom(sin), lastUpdateTime = sin.readInstant(), enabledTime = sin.readOptionalInstant(), - schemaVersion = sin.readInt(), triggers = sin.readList(TriggerV2::readFrom), + schemaVersion = sin.readInt(), + lookBackWindow = TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME), queryLanguage = sin.readEnum(QueryLanguage::class.java), query = sin.readString() ) @@ -112,8 +116,9 @@ data class PPLMonitor( builder.field(ENABLED_FIELD, enabled) builder.nonOptionalTimeField(LAST_UPDATE_TIME_FIELD, lastUpdateTime) builder.optionalTimeField(ENABLED_TIME_FIELD, enabledTime) - builder.field(SCHEMA_VERSION_FIELD, schemaVersion) builder.field(TRIGGERS_FIELD, triggers.toTypedArray()) + builder.field(SCHEMA_VERSION_FIELD, schemaVersion) + builder.field(LOOK_BACK_WINDOW_FIELD, lookBackWindow?.toHumanReadableString(0)) builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) builder.field(QUERY_FIELD, query) @@ -142,12 +147,16 @@ data class PPLMonitor( } out.writeInstant(lastUpdateTime) out.writeOptionalInstant(enabledTime) - out.writeInt(schemaVersion) out.writeVInt(triggers.size) triggers.forEach { out.writeEnum(TriggerV2.TriggerV2Type.PPL_TRIGGER) it.writeTo(out) } + out.writeInt(schemaVersion) + + out.writeBoolean(lookBackWindow != null) + lookBackWindow?.let { out.writeString(lookBackWindow.toHumanReadableString(0)) } + out.writeEnum(queryLanguage) out.writeString(query) } @@ -162,6 +171,7 @@ data class PPLMonitor( LAST_UPDATE_TIME_FIELD to lastUpdateTime.toEpochMilli(), ENABLED_TIME_FIELD to enabledTime?.toEpochMilli(), TRIGGERS_FIELD to triggers, + LOOK_BACK_WINDOW_FIELD to lookBackWindow?.toHumanReadableString(0), QUERY_LANGUAGE_FIELD to queryLanguage.value, QUERY_FIELD to query ) @@ -188,6 +198,11 @@ data class PPLMonitor( const val QUERY_LANGUAGE_FIELD = "query_language" const val QUERY_FIELD = "query" + // mock setting name used when parsing TimeValue + // TimeValue class is usually reserved for declaring settings, but we're using it + // outside that use case here, which is why we need these placeholders + private const val PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME = "ppl_monitor_look_back_window" + @JvmStatic @JvmOverloads @Throws(IOException::class) @@ -198,8 +213,9 @@ data class PPLMonitor( var schedule: Schedule? = null var lastUpdateTime: Instant? = null var enabledTime: Instant? = null - var schemaVersion = NO_SCHEMA_VERSION val triggers: MutableList = mutableListOf() + var schemaVersion = NO_SCHEMA_VERSION + var lookBackWindow: TimeValue? = null var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL var query: String? = null @@ -216,7 +232,6 @@ data class PPLMonitor( SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() ENABLED_TIME_FIELD -> enabledTime = xcp.instant() - SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() TRIGGERS_FIELD -> { XContentParserUtils.ensureExpectedToken( XContentParser.Token.START_ARRAY, @@ -227,6 +242,15 @@ data class PPLMonitor( triggers.add(PPLTrigger.parseInner(xcp)) } } + SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() + LOOK_BACK_WINDOW_FIELD -> { + lookBackWindow = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + val input = xcp.text() + TimeValue.parseTimeValue(input, PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME) // throws IllegalArgumentException if there's parsing error + } + } QUERY_LANGUAGE_FIELD -> { val input = xcp.text() val enumMatchResult = QueryLanguage.enumFromString(input) @@ -269,6 +293,10 @@ data class PPLMonitor( requireNotNull(query) { "Query is null" } requireNotNull(lastUpdateTime) { "Last update time is null" } + if (schedule is IntervalSchedule && lookBackWindow != null) { + throw IllegalArgumentException("Look back windows only supported for CRON schedules") + } + if (queryLanguage == QueryLanguage.SQL) { throw IllegalArgumentException("SQL queries are not supported. Please use a PPL query.") } @@ -282,8 +310,9 @@ data class PPLMonitor( schedule, lastUpdateTime, enabledTime, - schemaVersion, triggers, + schemaVersion, + lookBackWindow, queryLanguage, query ) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 540ca9bc..071733d2 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -164,6 +164,8 @@ data class PPLTrigger( const val CUSTOM_CONDITION_FIELD = "custom_condition" // mock setting name used when parsing TimeValue + // TimeValue class is usually reserved for declaring settings, but we're using it + // outside that use case here, which is why we need these placeholders private const val PLACEHOLDER_SUPPRESS_SETTING_NAME = "ppl_trigger_suppress_duration" private const val PLACEHOLDER_EXPIRE_SETTING_NAME = "ppl_trigger_expire_duration" From 5130b300d3fee7a10a58da7eb956fe2f358d5ca5 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 5 Sep 2025 10:55:22 -0700 Subject: [PATCH 15/30] adding query results to AlertV2 --- .../org/opensearch/commons/alerting/model/AlertV2.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index 935f4ddd..abb5ced8 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -46,6 +46,7 @@ data class AlertV2( // val monitorUser: User?, val triggerId: String, val triggerName: String, + val queryResults: Map, val state: State, // TODO: potentially delete for stateless alerts, all stateless alerts are ACTIVE val startTime: Instant, // TODO: potentially delete for stateless alerts val endTime: Instant? = null, // TODO: potentially delete for stateless alerts @@ -73,6 +74,7 @@ data class AlertV2( // }, triggerId = sin.readString(), triggerName = sin.readString(), + queryResults = sin.readMap()!!.toMap(), state = sin.readEnum(State::class.java), startTime = sin.readInstant(), endTime = sin.readOptionalInstant(), @@ -98,6 +100,7 @@ data class AlertV2( // monitorUser?.writeTo(out) out.writeString(triggerId) out.writeString(triggerName) + out.writeMap(queryResults) out.writeEnum(state) out.writeInstant(startTime) out.writeOptionalInstant(endTime) @@ -122,6 +125,7 @@ data class AlertV2( .field(EXECUTION_ID_FIELD, executionId) .field(TRIGGER_ID_FIELD, triggerId) .field(TRIGGER_NAME_FIELD, triggerName) + .field(QUERY_RESULTS_FIELD, queryResults) .field(STATE_FIELD, state) .field(ERROR_MESSAGE_FIELD, errorMessage) .field(ALERT_HISTORY_FIELD, errorHistory.toTypedArray()) @@ -159,6 +163,7 @@ data class AlertV2( companion object { const val EXPIRATION_TIME_FIELD = "expiration_time" + const val QUERY_RESULTS_FIELD = "query_results" @JvmStatic @JvmOverloads @@ -171,6 +176,7 @@ data class AlertV2( // var monitorUser: User? = null lateinit var triggerId: String lateinit var triggerName: String + var queryResults: Map = mapOf() lateinit var state: State lateinit var startTime: Instant lateinit var severity: String @@ -202,6 +208,7 @@ data class AlertV2( TRIGGER_ID_FIELD -> triggerId = xcp.text() STATE_FIELD -> state = State.valueOf(xcp.text()) TRIGGER_NAME_FIELD -> triggerName = xcp.text() + QUERY_RESULTS_FIELD -> queryResults = xcp.map() START_TIME_FIELD -> startTime = requireNotNull(xcp.instant()) END_TIME_FIELD -> endTime = xcp.instant() EXPIRATION_TIME_FIELD -> expirationTime = xcp.instant() @@ -235,6 +242,7 @@ data class AlertV2( // monitorUser = monitorUser, triggerId = requireNotNull(triggerId), triggerName = requireNotNull(triggerName), + queryResults = requireNotNull(queryResults), state = requireNotNull(state), startTime = requireNotNull(startTime), endTime = endTime, From 4ee1f6f00fb9d52b10d8b9b8077e960448256e6c Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 5 Sep 2025 13:29:10 -0700 Subject: [PATCH 16/30] initial jar dependency changes --- build.gradle | 44 +++++- .../commons/ppl/action/PPLQueryAction.java | 18 --- .../ppl/action/TransportPPLQueryRequest.java | 139 ------------------ .../ppl/action/TransportPPLQueryResponse.java | 51 ------- .../commons/ppl/format/ErrorFormatter.java | 53 ------- .../opensearch/commons/ppl/format/Format.java | 63 -------- .../ppl/format/JsonResponseFormatter.java | 64 -------- .../commons/ppl/format/ResponseFormatter.java | 33 ----- .../commons/ppl/serde/DataSourceType.java | 61 -------- .../commons/ppl/serde/SerializeUtils.java | 48 ------ .../commons/ppl/util/PPLQueryRequest.java | 77 ---------- .../ppl/util/PPLQueryRequestFactory.java | 115 --------------- .../commons/ppl/PPLPluginInterface.kt | 8 +- 13 files changed, 42 insertions(+), 732 deletions(-) delete mode 100644 src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/format/Format.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java delete mode 100644 src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java diff --git a/build.gradle b/build.gradle index d615e0c5..879b094b 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,14 @@ buildscript { isSnapshot = "true" == System.getProperty("build.snapshot", "true") buildVersionQualifier = System.getProperty("build.version_qualifier", "") kotlin_version = System.getProperty("kotlin.version", "1.9.25") + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + if (buildVersionQualifier) { + opensearch_build += "-${buildVersionQualifier}" + } + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } } repositories { @@ -68,6 +76,7 @@ apply from: 'build-tools/opensearchplugin-coverage.gradle' apply plugin: 'opensearch.java-agent' configurations { + zipArchive ktlint { resolutionStrategy { force "ch.qos.logback:logback-classic:1.5.16" @@ -76,6 +85,18 @@ configurations { } } +def sqlJarDirectory = "$buildDir/dependencies/opensearch-sql-plugin" + +task addJarsToClasspath(type: Copy) { + from(fileTree(dir: sqlJarDirectory)) { + include "opensearch-sql-${opensearch_build}.jar" + include "ppl-${opensearch_build}.jar" + include "protocol-${opensearch_build}.jar" + include "core-${opensearch_build}.jar" + } + into("$buildDir/classes") +} + dependencies { compileOnly "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" compileOnly "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" @@ -91,16 +112,22 @@ dependencies { testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.cronutils:cron-utils:9.1.6" testImplementation "commons-validator:commons-validator:1.7" - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.4' - implementation 'org.json:json:20240303' - implementation "com.google.guava:guava:33.3.0-jre" - implementation 'com.google.code.gson:gson:2.10.1' - compileOnly 'org.projectlombok:lombok:1.18.38' - annotationProcessor 'org.projectlombok:lombok:1.18.38' + + implementation fileTree(dir: sqlJarDirectory, include: ["opensearch-sql-${opensearch_build}.jar", "ppl-${opensearch_build}.jar", "protocol-${opensearch_build}.jar", "core-${opensearch_build}.jar"]) + + zipArchive group: 'org.opensearch.plugin', name:'opensearch-sql-plugin', version: "${opensearch_build}" ktlint "com.pinterest:ktlint:0.47.1" } +task extractSqlJar(type: Copy) { + mustRunAfter() + from(zipTree(configurations.zipArchive.find { it.name.startsWith("opensearch-sql-plugin") })) + into sqlJarDirectory +} + +tasks.addJarsToClasspath.dependsOn(extractSqlJar) + test { useJUnitPlatform() testLogging { @@ -147,7 +174,12 @@ tasks.register('ktlintFormat', JavaExec) { args "-F", "src/**/*.kt" } +compileJava { + dependsOn extractSqlJar +} + compileKotlin { + dependsOn extractSqlJar kotlinOptions { freeCompilerArgs = ['-Xjsr305=strict'] jvmTarget = "21" diff --git a/src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java b/src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java deleted file mode 100644 index e1e8ef36..00000000 --- a/src/main/java/org/opensearch/commons/ppl/action/PPLQueryAction.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.action; - -import org.opensearch.action.ActionType; - -public class PPLQueryAction extends ActionType { - // Internal Action which is not used for public facing RestAPIs. - public static final String NAME = "cluster:admin/opensearch/ppl"; - public static final PPLQueryAction INSTANCE = new PPLQueryAction(); - - private PPLQueryAction() { - super(NAME, TransportPPLQueryResponse::new); - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java deleted file mode 100644 index ddddcfcd..00000000 --- a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryRequest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.action; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Locale; -import java.util.Optional; - -import org.json.JSONObject; -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.commons.ppl.format.Format; -import org.opensearch.commons.ppl.format.JsonResponseFormatter; -import org.opensearch.commons.ppl.util.PPLQueryRequest; -import org.opensearch.core.common.io.stream.InputStreamStreamInput; -import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - -//private val log = LogManager.getLogger(TransportExecuteMonitorAction::class.java) - -@RequiredArgsConstructor -public class TransportPPLQueryRequest extends ActionRequest { - public static final TransportPPLQueryRequest NULL = new TransportPPLQueryRequest("", null, ""); - private final String pplQuery; - @Getter - private final JSONObject jsonContent; - - @Getter - private final String path; - - @Getter - private String format = ""; - - @Setter - @Getter - @Accessors(fluent = true) - private boolean sanitize = true; - - @Setter - @Getter - @Accessors(fluent = true) - private JsonResponseFormatter.Style style = JsonResponseFormatter.Style.COMPACT; - - /** Constructor of TransportPPLQueryRequest from PPLQueryRequest. */ - public TransportPPLQueryRequest(PPLQueryRequest pplQueryRequest) { - pplQuery = pplQueryRequest.getRequest(); - jsonContent = pplQueryRequest.getJsonContent(); - path = pplQueryRequest.getPath(); - format = pplQueryRequest.getFormat(); - sanitize = pplQueryRequest.sanitize(); - style = pplQueryRequest.style(); - } - - /** Constructor of TransportPPLQueryRequest from StreamInput. */ - public TransportPPLQueryRequest(StreamInput in) throws IOException { - super(in); - pplQuery = in.readOptionalString(); - format = in.readOptionalString(); - String jsonContentString = in.readOptionalString(); - jsonContent = jsonContentString != null ? new JSONObject(jsonContentString) : null; - path = in.readOptionalString(); - sanitize = in.readBoolean(); - style = in.readEnum(JsonResponseFormatter.Style.class); - } - - /** Re-create the object from the actionRequest. */ - public static TransportPPLQueryRequest fromActionRequest(final ActionRequest actionRequest) { - if (actionRequest instanceof TransportPPLQueryRequest) { - return (TransportPPLQueryRequest) actionRequest; - } - - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { - actionRequest.writeTo(osso); - try (InputStreamStreamInput input = new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { - return new TransportPPLQueryRequest(input); - } - } catch (IOException e) { - throw new IllegalArgumentException("failed to parse ActionRequest into TransportPPLQueryRequest", e); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeOptionalString(pplQuery); - out.writeOptionalString(format); - out.writeOptionalString(jsonContent != null ? jsonContent.toString() : null); - out.writeOptionalString(path); - out.writeBoolean(sanitize); - out.writeEnum(style); - } - - public String getRequest() { - return pplQuery; - } - - /** - * Check if request is to explain rather than execute the query. - * - * @return true if it is an explain request - */ - public boolean isExplainRequest() { - return path.endsWith("/_explain"); - } - - /** Decide on the formatter by the requested format. */ - public Format format() { - Optional optionalFormat = Format.of(format); - if (optionalFormat.isPresent()) { - return optionalFormat.get(); - } else { - throw new IllegalArgumentException(String.format(Locale.ROOT, "response in %s format is not supported.", format)); - } - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - /** Convert to PPLQueryRequest. */ - public PPLQueryRequest toPPLQueryRequest() { - PPLQueryRequest pplQueryRequest = new PPLQueryRequest(pplQuery, jsonContent, path, format); - pplQueryRequest.sanitize(sanitize); - pplQueryRequest.style(style); - return pplQueryRequest; - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java b/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java deleted file mode 100644 index 7cd13194..00000000 --- a/src/main/java/org/opensearch/commons/ppl/action/TransportPPLQueryResponse.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.action; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.InputStreamStreamInput; -import org.opensearch.core.common.io.stream.OutputStreamStreamOutput; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class TransportPPLQueryResponse extends ActionResponse { - @Getter - public final String result; - - public TransportPPLQueryResponse(StreamInput in) throws IOException { - super(in); - result = in.readString(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(result); - } - - public static TransportPPLQueryResponse fromActionResponse(ActionResponse actionResponse) { - if (actionResponse instanceof TransportPPLQueryResponse) { - return (TransportPPLQueryResponse) actionResponse; - } - - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamStreamOutput osso = new OutputStreamStreamOutput(baos)) { - actionResponse.writeTo(osso); - try (StreamInput input = new InputStreamStreamInput(new ByteArrayInputStream(baos.toByteArray()))) { - return new TransportPPLQueryResponse(input); - } - } catch (IOException e) { - throw new UncheckedIOException("failed to parse ActionResponse into TransportPPLQueryResponse", e); - } - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java deleted file mode 100644 index 3286ef1f..00000000 --- a/src/main/java/org/opensearch/commons/ppl/format/ErrorFormatter.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.format; - -import java.security.AccessController; -import java.security.PrivilegedAction; - -import org.opensearch.commons.ppl.serde.SerializeUtils; - -import com.google.gson.Gson; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.experimental.UtilityClass; - -@UtilityClass -public class ErrorFormatter { - - private static final Gson PRETTY_PRINT_GSON = AccessController - .doPrivileged((PrivilegedAction) () -> SerializeUtils.getGsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()); - private static final Gson GSON = AccessController - .doPrivileged((PrivilegedAction) () -> SerializeUtils.getGsonBuilder().disableHtmlEscaping().create()); - - /** Util method to format {@link Throwable} response to JSON string in compact printing. */ - public static String compactFormat(Throwable t) { - JsonError error = new ErrorFormatter.JsonError(t.getClass().getSimpleName(), t.getMessage()); - return compactJsonify(error); - } - - /** Util method to format {@link Throwable} response to JSON string in pretty printing. */ - public static String prettyFormat(Throwable t) { - JsonError error = new ErrorFormatter.JsonError(t.getClass().getSimpleName(), t.getMessage()); - return prettyJsonify(error); - } - - public static String compactJsonify(Object jsonObject) { - return AccessController.doPrivileged((PrivilegedAction) () -> GSON.toJson(jsonObject)); - } - - public static String prettyJsonify(Object jsonObject) { - return AccessController.doPrivileged((PrivilegedAction) () -> PRETTY_PRINT_GSON.toJson(jsonObject)); - } - - @RequiredArgsConstructor - @Getter - public static class JsonError { - private final String type; - private final String reason; - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/format/Format.java b/src/main/java/org/opensearch/commons/ppl/format/Format.java deleted file mode 100644 index c6d48229..00000000 --- a/src/main/java/org/opensearch/commons/ppl/format/Format.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.format; - -import java.util.Locale; -import java.util.Map; -import java.util.Optional; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public enum Format { - JDBC("jdbc"), - CSV("csv"), - RAW("raw"), - VIZ("viz"), - // format of explain response - SIMPLE("simple"), - STANDARD("standard"), - EXTENDED("extended"), - COST("cost"); - - @Getter - private final String formatName; - - public static final Map RESPONSE_FORMATS; - - public static final Map EXPLAIN_FORMATS; - - static { - ImmutableMap.Builder builder; - builder = new ImmutableMap.Builder<>(); - builder.put(JDBC.formatName, JDBC); - builder.put(CSV.formatName, CSV); - builder.put(RAW.formatName, RAW); - builder.put(VIZ.formatName, VIZ); - RESPONSE_FORMATS = builder.build(); - - builder = new ImmutableMap.Builder<>(); - builder.put(SIMPLE.formatName, SIMPLE); - builder.put(STANDARD.formatName, STANDARD); - builder.put(EXTENDED.formatName, EXTENDED); - builder.put(COST.formatName, COST); - EXPLAIN_FORMATS = builder.build(); - } - - public static Optional of(String formatName) { - String format = Strings.isNullOrEmpty(formatName) ? "jdbc" : formatName.toLowerCase(Locale.ROOT); - return Optional.ofNullable(RESPONSE_FORMATS.getOrDefault(format, null)); - } - - public static Optional ofExplain(String formatName) { - String format = Strings.isNullOrEmpty(formatName) ? "standard" : formatName.toLowerCase(Locale.ROOT); - return Optional.ofNullable(EXPLAIN_FORMATS.getOrDefault(format, null)); - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java deleted file mode 100644 index 7021f2ed..00000000 --- a/src/main/java/org/opensearch/commons/ppl/format/JsonResponseFormatter.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.format; - -import static org.opensearch.commons.ppl.format.ErrorFormatter.compactFormat; -import static org.opensearch.commons.ppl.format.ErrorFormatter.compactJsonify; -import static org.opensearch.commons.ppl.format.ErrorFormatter.prettyFormat; -import static org.opensearch.commons.ppl.format.ErrorFormatter.prettyJsonify; -import static org.opensearch.commons.ppl.format.JsonResponseFormatter.Style.PRETTY; - -import java.security.AccessController; -import java.security.PrivilegedAction; - -import lombok.RequiredArgsConstructor; - -/** - * Abstract class for all JSON formatter. - * - * @param response generic type which could be DQL or DML response - */ -@RequiredArgsConstructor -public abstract class JsonResponseFormatter implements ResponseFormatter { - - /** JSON format styles: pretty format or compact format without indent and space. */ - public enum Style { - PRETTY, - COMPACT - } - - /** JSON format style. */ - private final Style style; - - public static final String CONTENT_TYPE = "application/json; charset=UTF-8"; - - @Override - public String format(R response) { - return jsonify(buildJsonObject(response)); - } - - @Override - public String format(Throwable t) { - return AccessController.doPrivileged((PrivilegedAction) () -> (style == PRETTY) ? prettyFormat(t) : compactFormat(t)); - } - - public String contentType() { - return CONTENT_TYPE; - } - - /** - * Build JSON object to generate response json string. - * - * @param response response - * @return json object for response - */ - protected abstract Object buildJsonObject(R response); - - protected String jsonify(Object jsonObject) { - return AccessController - .doPrivileged((PrivilegedAction) () -> (style == PRETTY) ? prettyJsonify(jsonObject) : compactJsonify(jsonObject)); - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java b/src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java deleted file mode 100644 index 1e257680..00000000 --- a/src/main/java/org/opensearch/commons/ppl/format/ResponseFormatter.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.format; - -/** Response formatter to format response to different formats. */ -public interface ResponseFormatter { - - /** - * Format response into string in expected format. - * - * @param response response - * @return string with response content formatted - */ - String format(R response); - - /** - * Format an exception into string. - * - * @param t exception occurred - * @return string with exception content formatted - */ - String format(Throwable t); - - /** - * Getter for the content type header of the response. - * - * @return string - */ - String contentType(); -} diff --git a/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java b/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java deleted file mode 100644 index 776f02fa..00000000 --- a/src/main/java/org/opensearch/commons/ppl/serde/DataSourceType.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.serde; - -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@EqualsAndHashCode -public class DataSourceType { - public static final DataSourceType PROMETHEUS = new DataSourceType("PROMETHEUS"); - public static final DataSourceType OPENSEARCH = new DataSourceType("OPENSEARCH"); - public static final DataSourceType S3GLUE = new DataSourceType("S3GLUE"); - public static final DataSourceType SECURITY_LAKE = new DataSourceType("SECURITY_LAKE"); - - // Map from uppercase DataSourceType name to DataSourceType object - private static final Map knownValues = new HashMap<>(); - - static { - register(PROMETHEUS, OPENSEARCH, S3GLUE, SECURITY_LAKE); - } - - private final String name; - - public String name() { - return name; - } - - @Override - public String toString() { - return name; - } - - /** Register DataSourceType to be used in fromString method */ - public static void register(DataSourceType... dataSourceTypes) { - for (DataSourceType type : dataSourceTypes) { - String upperCaseName = type.name().toUpperCase(Locale.ROOT); - if (!knownValues.containsKey(upperCaseName)) { - knownValues.put(type.name().toUpperCase(Locale.ROOT), type); - } else { - throw new IllegalArgumentException("DataSourceType with name " + type.name() + " already exists"); - } - } - } - - public static DataSourceType fromString(String name) { - String upperCaseName = name.toUpperCase(Locale.ROOT); - if (knownValues.containsKey(upperCaseName)) { - return knownValues.get(upperCaseName); - } else { - throw new IllegalArgumentException("No DataSourceType with name " + name + " found"); - } - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java b/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java deleted file mode 100644 index fb992f59..00000000 --- a/src/main/java/org/opensearch/commons/ppl/serde/SerializeUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.serde; - -import java.lang.reflect.Type; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class SerializeUtils { - private static class DataSourceTypeSerializer implements JsonSerializer { - @Override - public JsonElement serialize(DataSourceType dataSourceType, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(dataSourceType.name()); - } - } - - private static class DataSourceTypeDeserializer implements JsonDeserializer { - @Override - public DataSourceType deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) - throws JsonParseException { - return DataSourceType.fromString(jsonElement.getAsString()); - } - } - - public static GsonBuilder getGsonBuilder() { - return new GsonBuilder() - .registerTypeAdapter(DataSourceType.class, new DataSourceTypeSerializer()) - .registerTypeAdapter(DataSourceType.class, new DataSourceTypeDeserializer()); - } - - public static Gson buildGson() { - return getGsonBuilder().create(); - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java deleted file mode 100644 index 14305ac3..00000000 --- a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.util; - -import java.util.Locale; -import java.util.Optional; - -import org.json.JSONObject; -import org.opensearch.commons.ppl.format.Format; -import org.opensearch.commons.ppl.format.JsonResponseFormatter; - -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - -public class PPLQueryRequest { - - private static final String DEFAULT_PPL_PATH = "/_plugins/_ppl"; - - public static final PPLQueryRequest NULL = new PPLQueryRequest("", null, DEFAULT_PPL_PATH, ""); - - private final String pplQuery; - @Getter - private final JSONObject jsonContent; - @Getter - private final String path; - @Getter - private String format = ""; - - @Setter - @Getter - @Accessors(fluent = true) - private boolean sanitize = true; - - @Setter - @Getter - @Accessors(fluent = true) - private JsonResponseFormatter.Style style = JsonResponseFormatter.Style.COMPACT; - - public PPLQueryRequest(String pplQuery, JSONObject jsonContent, String path) { - this(pplQuery, jsonContent, path, ""); - } - - /** Constructor of PPLQueryRequest. */ - public PPLQueryRequest(String pplQuery, JSONObject jsonContent, String path, String format) { - this.pplQuery = pplQuery; - this.jsonContent = jsonContent; - this.path = Optional.ofNullable(path).orElse(DEFAULT_PPL_PATH); - this.format = format; - } - - public String getRequest() { - return pplQuery; - } - - /** - * Check if request is to explain rather than execute the query. - * - * @return true if it is a explain request - */ - public boolean isExplainRequest() { - return path.endsWith("/_explain"); - } - - /** Decide on the formatter by the requested format. */ - public Format format() { - Optional optionalFormat = Format.of(format); - if (optionalFormat.isPresent()) { - return optionalFormat.get(); - } else { - throw new IllegalArgumentException(String.format(Locale.ROOT, "response in %s format is not supported.", format)); - } - } -} diff --git a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java b/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java deleted file mode 100644 index afce77ea..00000000 --- a/src/main/java/org/opensearch/commons/ppl/util/PPLQueryRequestFactory.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.commons.ppl.util; - -import java.util.Locale; -import java.util.Map; -import java.util.Optional; - -import org.json.JSONException; -import org.json.JSONObject; -import org.opensearch.commons.ppl.format.Format; -import org.opensearch.commons.ppl.format.JsonResponseFormatter; -import org.opensearch.rest.RestRequest; - -/** Factory of {@link PPLQueryRequest}. */ -public class PPLQueryRequestFactory { - private static final String PPL_URL_PARAM_KEY = "ppl"; - private static final String PPL_FIELD_NAME = "query"; - private static final String QUERY_PARAMS_FORMAT = "format"; - private static final String QUERY_PARAMS_SANITIZE = "sanitize"; - private static final String DEFAULT_RESPONSE_FORMAT = "jdbc"; - private static final String DEFAULT_EXPLAIN_FORMAT = "standard"; - private static final String QUERY_PARAMS_PRETTY = "pretty"; - - /** - * Build {@link PPLQueryRequest} from {@link RestRequest}. - * - * @param request {@link PPLQueryRequest} - * @return {@link RestRequest} - */ - public static PPLQueryRequest getPPLRequest(RestRequest request) { - switch (request.method()) { - case GET: - return parsePPLRequestFromUrl(request); - case POST: - return parsePPLRequestFromPayload(request); - default: - throw new IllegalArgumentException("OpenSearch PPL doesn't supported HTTP " + request.method().name()); - } - } - - private static PPLQueryRequest parsePPLRequestFromUrl(RestRequest restRequest) { - String ppl; - - ppl = restRequest.param(PPL_URL_PARAM_KEY); - if (ppl == null) { - throw new IllegalArgumentException("Cannot find ppl parameter from the URL"); - } - return new PPLQueryRequest(ppl, null, restRequest.path()); - } - - private static PPLQueryRequest parsePPLRequestFromPayload(RestRequest restRequest) { - String content = restRequest.content().utf8ToString(); - JSONObject jsonContent; - Format format = getFormat(restRequest.params(), restRequest.rawPath()); - boolean pretty = getPrettyOption(restRequest.params()); - try { - jsonContent = new JSONObject(content); - PPLQueryRequest pplRequest = new PPLQueryRequest( - jsonContent.getString(PPL_FIELD_NAME), - jsonContent, - restRequest.path(), - format.getFormatName() - ); - // set sanitize option if csv format - if (format.equals(Format.CSV)) { - pplRequest.sanitize(getSanitizeOption(restRequest.params())); - } - // set pretty option - if (pretty) { - pplRequest.style(JsonResponseFormatter.Style.PRETTY); - } - return pplRequest; - } catch (JSONException e) { - throw new IllegalArgumentException("Failed to parse request payload", e); - } - } - - private static Format getFormat(Map requestParams, String path) { - String formatName = requestParams.containsKey(QUERY_PARAMS_FORMAT) ? requestParams.get(QUERY_PARAMS_FORMAT).toLowerCase(Locale.ROOT) - : isExplainRequest(path) ? DEFAULT_EXPLAIN_FORMAT - : DEFAULT_RESPONSE_FORMAT; - Optional optionalFormat = isExplainRequest(path) ? Format.ofExplain(formatName) : Format.of(formatName); - if (optionalFormat.isPresent()) { - return optionalFormat.get(); - } else { - throw new IllegalArgumentException("Failed to create executor due to unknown response format: " + formatName); - } - } - - private static boolean isExplainRequest(String path) { - return path != null && path.endsWith("/_explain"); - } - - private static boolean getSanitizeOption(Map requestParams) { - if (requestParams.containsKey(QUERY_PARAMS_SANITIZE)) { - return Boolean.parseBoolean(requestParams.get(QUERY_PARAMS_SANITIZE)); - } - return true; - } - - private static boolean getPrettyOption(Map requestParams) { - if (requestParams.containsKey(QUERY_PARAMS_PRETTY)) { - String prettyValue = requestParams.get(QUERY_PARAMS_PRETTY); - if (prettyValue.isEmpty()) { - return true; - } - return Boolean.parseBoolean(prettyValue); - } - return false; - } -} diff --git a/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt index 2b7a2709..f03ec056 100644 --- a/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt +++ b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt @@ -1,12 +1,12 @@ package org.opensearch.commons.ppl -import org.opensearch.commons.ppl.action.PPLQueryAction -import org.opensearch.commons.ppl.action.TransportPPLQueryRequest -import org.opensearch.commons.ppl.action.TransportPPLQueryResponse import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener import org.opensearch.core.action.ActionResponse import org.opensearch.core.common.io.stream.Writeable +import org.opensearch.sql.plugin.transport.PPLQueryAction +import org.opensearch.sql.plugin.transport.TransportPPLQueryRequest +import org.opensearch.sql.plugin.transport.TransportPPLQueryResponse import org.opensearch.transport.client.node.NodeClient /** @@ -32,7 +32,7 @@ object PPLPluginInterface { * the response object. */ @Suppress("UNCHECKED_CAST") - private fun wrapActionListener( + private fun wrapActionListener( listener: ActionListener, recreate: (Writeable) -> Response ): ActionListener { From af434ad3eca335ed35b805cde7c4dd76019dcad2 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sat, 6 Sep 2025 10:29:51 -0700 Subject: [PATCH 17/30] including SQL jar dependencies and various cleanups --- build.gradle | 24 ++++++++++-- .../commons/alerting/model/PPLMonitor.kt | 38 +++++++++---------- .../alerting/model/PPLTriggerRunResult.kt | 2 +- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index 879b094b..727d92ed 100644 --- a/build.gradle +++ b/build.gradle @@ -112,8 +112,9 @@ dependencies { testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.cronutils:cron-utils:9.1.6" testImplementation "commons-validator:commons-validator:1.7" + implementation 'org.json:json:20240303' - implementation fileTree(dir: sqlJarDirectory, include: ["opensearch-sql-${opensearch_build}.jar", "ppl-${opensearch_build}.jar", "protocol-${opensearch_build}.jar", "core-${opensearch_build}.jar"]) + implementation fileTree(dir: sqlJarDirectory, include: ["opensearch-sql-thin-${opensearch_build}.jar", "ppl-${opensearch_build}.jar", "protocol-${opensearch_build}.jar", "core-${opensearch_build}.jar"]) zipArchive group: 'org.opensearch.plugin', name:'opensearch-sql-plugin', version: "${opensearch_build}" @@ -126,7 +127,22 @@ task extractSqlJar(type: Copy) { into sqlJarDirectory } -tasks.addJarsToClasspath.dependsOn(extractSqlJar) +task extractSqlClass(type: Copy, dependsOn: [extractSqlJar]) { + from zipTree("${sqlJarDirectory}/opensearch-sql-${opensearch_build}.jar") + into("$buildDir/opensearch-sql") + include 'org/opensearch/sql/**' +} + +task replaceSqlJar(type: Jar, dependsOn: [extractSqlClass]) { + from("$buildDir/opensearch-sql") + archiveFileName = "opensearch-sql-thin-${opensearch_build}.jar" + destinationDirectory = file(sqlJarDirectory) + doLast { + file("${sqlJarDirectory}/opensearch-sql-${opensearch_build}.jar").delete() + } +} + +tasks.addJarsToClasspath.dependsOn(replaceSqlJar) test { useJUnitPlatform() @@ -175,11 +191,11 @@ tasks.register('ktlintFormat', JavaExec) { } compileJava { - dependsOn extractSqlJar + dependsOn addJarsToClasspath } compileKotlin { - dependsOn extractSqlJar + dependsOn addJarsToClasspath kotlinOptions { freeCompilerArgs = ['-Xjsr305=strict'] jvmTarget = "21" diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index fff5b4bb..a318cf4d 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -42,11 +42,11 @@ data class PPLMonitor( override val name: String, override val enabled: Boolean, override val schedule: Schedule, + override val lookBackWindow: TimeValue? = null, override val lastUpdateTime: Instant, override val enabledTime: Instant?, - override val triggers: List, + override val triggers: List, // TODO: change this to list of PPLTriggers override val schemaVersion: Int = NO_SCHEMA_VERSION, - override val lookBackWindow: TimeValue? = null, val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported val query: String ) : MonitorV2 { @@ -88,11 +88,11 @@ data class PPLMonitor( name = sin.readString(), enabled = sin.readBoolean(), schedule = Schedule.readFrom(sin), + lookBackWindow = TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME), lastUpdateTime = sin.readInstant(), enabledTime = sin.readOptionalInstant(), triggers = sin.readList(TriggerV2::readFrom), schemaVersion = sin.readInt(), - lookBackWindow = TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME), queryLanguage = sin.readEnum(QueryLanguage::class.java), query = sin.readString() ) @@ -113,12 +113,12 @@ data class PPLMonitor( builder.field(NAME_FIELD, name) builder.field(SCHEDULE_FIELD, schedule) + builder.field(LOOK_BACK_WINDOW_FIELD, lookBackWindow?.toHumanReadableString(0)) 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(LOOK_BACK_WINDOW_FIELD, lookBackWindow?.toHumanReadableString(0)) builder.field(QUERY_LANGUAGE_FIELD, queryLanguage.value) builder.field(QUERY_FIELD, query) @@ -145,6 +145,10 @@ data class PPLMonitor( } else { out.writeEnum(Schedule.TYPE.INTERVAL) } + + out.writeBoolean(lookBackWindow != null) + lookBackWindow?.let { out.writeString(lookBackWindow.toHumanReadableString(0)) } + out.writeInstant(lastUpdateTime) out.writeOptionalInstant(enabledTime) out.writeVInt(triggers.size) @@ -153,10 +157,6 @@ data class PPLMonitor( it.writeTo(out) } out.writeInt(schemaVersion) - - out.writeBoolean(lookBackWindow != null) - lookBackWindow?.let { out.writeString(lookBackWindow.toHumanReadableString(0)) } - out.writeEnum(queryLanguage) out.writeString(query) } @@ -168,10 +168,10 @@ data class PPLMonitor( NAME_FIELD to name, ENABLED_FIELD to enabled, SCHEDULE_FIELD to schedule, + LOOK_BACK_WINDOW_FIELD to lookBackWindow?.toHumanReadableString(0), LAST_UPDATE_TIME_FIELD to lastUpdateTime.toEpochMilli(), ENABLED_TIME_FIELD to enabledTime?.toEpochMilli(), TRIGGERS_FIELD to triggers, - LOOK_BACK_WINDOW_FIELD to lookBackWindow?.toHumanReadableString(0), QUERY_LANGUAGE_FIELD to queryLanguage.value, QUERY_FIELD to query ) @@ -211,11 +211,11 @@ data class PPLMonitor( var monitorType: String = PPL_MONITOR_TYPE var enabled = true var schedule: Schedule? = null + var lookBackWindow: TimeValue? = null var lastUpdateTime: Instant? = null var enabledTime: Instant? = null val triggers: MutableList = mutableListOf() var schemaVersion = NO_SCHEMA_VERSION - var lookBackWindow: TimeValue? = null var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL var query: String? = null @@ -230,6 +230,14 @@ data class PPLMonitor( MONITOR_TYPE_FIELD -> monitorType = xcp.text() ENABLED_FIELD -> enabled = xcp.booleanValue() SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) + LOOK_BACK_WINDOW_FIELD -> { + lookBackWindow = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { + null + } else { + val input = xcp.text() + TimeValue.parseTimeValue(input, PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME) // throws IllegalArgumentException if there's parsing error + } + } LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() ENABLED_TIME_FIELD -> enabledTime = xcp.instant() TRIGGERS_FIELD -> { @@ -243,14 +251,6 @@ data class PPLMonitor( } } SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() - LOOK_BACK_WINDOW_FIELD -> { - lookBackWindow = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { - val input = xcp.text() - TimeValue.parseTimeValue(input, PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME) // throws IllegalArgumentException if there's parsing error - } - } QUERY_LANGUAGE_FIELD -> { val input = xcp.text() val enumMatchResult = QueryLanguage.enumFromString(input) @@ -308,11 +308,11 @@ data class PPLMonitor( name, enabled, schedule, + lookBackWindow, lastUpdateTime, enabledTime, triggers, schemaVersion, - lookBackWindow, queryLanguage, query ) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt index 5d46dcf7..d9de4813 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt @@ -13,7 +13,7 @@ import java.time.Instant data class PPLTriggerRunResult( override var triggerName: String, - override var triggered: Boolean, // TODO: may need to change this based on whether trigger mode is result set or per result + override var triggered: Boolean, override var error: Exception?, var actionResults: MutableMap = mutableMapOf() ) : TriggerV2RunResult { From 95acd7c03b10f59238e70191501fc7300f7c3b05 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sat, 6 Sep 2025 11:13:12 -0700 Subject: [PATCH 18/30] cleaning up unneeded AlertV2 fields --- .../commons/alerting/model/AlertV2.kt | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index abb5ced8..d4022e2c 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -47,14 +47,8 @@ data class AlertV2( val triggerId: String, val triggerName: String, val queryResults: Map, - val state: State, // TODO: potentially delete for stateless alerts, all stateless alerts are ACTIVE - val startTime: Instant, // TODO: potentially delete for stateless alerts - val endTime: Instant? = null, // TODO: potentially delete for stateless alerts val expirationTime: Instant?, - val lastNotificationTime: Instant? = null, - val acknowledgedTime: Instant? = null, val errorMessage: String? = null, - val errorHistory: List, val severity: String, val actionExecutionResults: List, val executionId: String? = null @@ -75,14 +69,8 @@ data class AlertV2( triggerId = sin.readString(), triggerName = sin.readString(), queryResults = sin.readMap()!!.toMap(), - state = sin.readEnum(State::class.java), - startTime = sin.readInstant(), - endTime = sin.readOptionalInstant(), expirationTime = sin.readOptionalInstant(), - lastNotificationTime = sin.readOptionalInstant(), - acknowledgedTime = sin.readOptionalInstant(), errorMessage = sin.readOptionalString(), - errorHistory = sin.readList(::AlertError), severity = sin.readString(), actionExecutionResults = sin.readList(::ActionExecutionResult), executionId = sin.readOptionalString() @@ -101,14 +89,8 @@ data class AlertV2( out.writeString(triggerId) out.writeString(triggerName) out.writeMap(queryResults) - out.writeEnum(state) - out.writeInstant(startTime) - out.writeOptionalInstant(endTime) out.writeOptionalInstant(expirationTime) - out.writeOptionalInstant(lastNotificationTime) - out.writeOptionalInstant(acknowledgedTime) out.writeOptionalString(errorMessage) - out.writeCollection(errorHistory) out.writeString(severity) out.writeCollection(actionExecutionResults) out.writeOptionalString(executionId) @@ -126,16 +108,10 @@ data class AlertV2( .field(TRIGGER_ID_FIELD, triggerId) .field(TRIGGER_NAME_FIELD, triggerName) .field(QUERY_RESULTS_FIELD, queryResults) - .field(STATE_FIELD, state) .field(ERROR_MESSAGE_FIELD, errorMessage) - .field(ALERT_HISTORY_FIELD, errorHistory.toTypedArray()) .field(SEVERITY_FIELD, severity) .field(ACTION_EXECUTION_RESULTS_FIELD, actionExecutionResults.toTypedArray()) .optionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) - .optionalTimeField(START_TIME_FIELD, startTime) - .optionalTimeField(LAST_NOTIFICATION_TIME_FIELD, lastNotificationTime) - .optionalTimeField(END_TIME_FIELD, endTime) - .optionalTimeField(ACKNOWLEDGED_TIME_FIELD, acknowledgedTime) .endObject() // if (!secure) { @@ -147,17 +123,12 @@ data class AlertV2( fun asTemplateArg(): Map { return mapOf( - ACKNOWLEDGED_TIME_FIELD to acknowledgedTime?.toEpochMilli(), ALERT_ID_FIELD to id, ALERT_VERSION_FIELD to version, - END_TIME_FIELD to endTime?.toEpochMilli(), ERROR_MESSAGE_FIELD to errorMessage, EXECUTION_ID_FIELD to executionId, - LAST_NOTIFICATION_TIME_FIELD to lastNotificationTime?.toEpochMilli(), EXPIRATION_TIME_FIELD to expirationTime?.toEpochMilli(), SEVERITY_FIELD to severity, - START_TIME_FIELD to startTime.toEpochMilli(), - STATE_FIELD to state.toString() ) } @@ -177,16 +148,10 @@ data class AlertV2( lateinit var triggerId: String lateinit var triggerName: String var queryResults: Map = mapOf() - lateinit var state: State - lateinit var startTime: Instant lateinit var severity: String var expirationTime: Instant? = null - var endTime: Instant? = null - var lastNotificationTime: Instant? = null - var acknowledgedTime: Instant? = null var errorMessage: String? = null var executionId: String? = null - val errorHistory: MutableList = mutableListOf() val actionExecutionResults: MutableList = mutableListOf() ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) @@ -206,22 +171,11 @@ data class AlertV2( // User.parse(xcp) // } TRIGGER_ID_FIELD -> triggerId = xcp.text() - STATE_FIELD -> state = State.valueOf(xcp.text()) TRIGGER_NAME_FIELD -> triggerName = xcp.text() QUERY_RESULTS_FIELD -> queryResults = xcp.map() - START_TIME_FIELD -> startTime = requireNotNull(xcp.instant()) - END_TIME_FIELD -> endTime = xcp.instant() EXPIRATION_TIME_FIELD -> expirationTime = xcp.instant() - LAST_NOTIFICATION_TIME_FIELD -> lastNotificationTime = xcp.instant() - ACKNOWLEDGED_TIME_FIELD -> acknowledgedTime = xcp.instant() ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull() EXECUTION_ID_FIELD -> executionId = xcp.textOrNull() - ALERT_HISTORY_FIELD -> { - ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) - while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { - errorHistory.add(AlertError.parse(xcp)) - } - } SEVERITY_FIELD -> severity = xcp.text() ACTION_EXECUTION_RESULTS_FIELD -> { ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) @@ -243,14 +197,8 @@ data class AlertV2( triggerId = requireNotNull(triggerId), triggerName = requireNotNull(triggerName), queryResults = requireNotNull(queryResults), - state = requireNotNull(state), - startTime = requireNotNull(startTime), - endTime = endTime, expirationTime = expirationTime, - lastNotificationTime = lastNotificationTime, - acknowledgedTime = acknowledgedTime, errorMessage = errorMessage, - errorHistory = errorHistory, severity = severity, actionExecutionResults = actionExecutionResults, executionId = executionId From 9202c91a29629fca5ec4f1d3490b6a8e8d1ac1d4 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sat, 6 Sep 2025 11:42:52 -0700 Subject: [PATCH 19/30] removed redundant trigger type wrapping around trigger object --- .../commons/alerting/model/PPLMonitor.kt | 11 ++++------ .../commons/alerting/model/PPLTrigger.kt | 22 ++++++------------- .../commons/alerting/model/TriggerV2.kt | 9 -------- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index a318cf4d..ebc98d63 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -45,7 +45,7 @@ data class PPLMonitor( override val lookBackWindow: TimeValue? = null, override val lastUpdateTime: Instant, override val enabledTime: Instant?, - override val triggers: List, // TODO: change this to list of PPLTriggers + override val triggers: List, override val schemaVersion: Int = NO_SCHEMA_VERSION, val queryLanguage: QueryLanguage = QueryLanguage.PPL, // default to PPL, SQL not currently supported val query: String @@ -91,7 +91,7 @@ data class PPLMonitor( lookBackWindow = TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME), lastUpdateTime = sin.readInstant(), enabledTime = sin.readOptionalInstant(), - triggers = sin.readList(TriggerV2::readFrom), + triggers = sin.readList(PPLTrigger::readFrom), schemaVersion = sin.readInt(), queryLanguage = sin.readEnum(QueryLanguage::class.java), query = sin.readString() @@ -152,10 +152,7 @@ data class PPLMonitor( out.writeInstant(lastUpdateTime) out.writeOptionalInstant(enabledTime) out.writeVInt(triggers.size) - triggers.forEach { - out.writeEnum(TriggerV2.TriggerV2Type.PPL_TRIGGER) - it.writeTo(out) - } + triggers.forEach { it.writeTo(out) } out.writeInt(schemaVersion) out.writeEnum(queryLanguage) out.writeString(query) @@ -214,7 +211,7 @@ data class PPLMonitor( var lookBackWindow: TimeValue? = null var lastUpdateTime: Instant? = null var enabledTime: Instant? = null - val triggers: MutableList = mutableListOf() + val triggers: MutableList = mutableListOf() var schemaVersion = NO_SCHEMA_VERSION var queryLanguage: QueryLanguage = QueryLanguage.PPL // default to PPL var query: String? = null diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 071733d2..62d82a45 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -25,6 +25,7 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant +import org.opensearch.commons.alerting.model.TriggerV2.TriggerV2Type private val logger = LogManager.getLogger(PPLTrigger::class.java) @@ -87,7 +88,6 @@ data class PPLTrigger( override fun toXContent(builder: XContentBuilder, params: ToXContent.Params?): XContentBuilder { builder.startObject() - builder.startObject(PPL_TRIGGER_FIELD) builder.field(ID_FIELD, id) builder.field(NAME_FIELD, name) builder.field(SEVERITY_FIELD, severity.value) @@ -101,7 +101,6 @@ data class PPLTrigger( numResultsValue?.let { builder.field(NUM_RESULTS_VALUE_FIELD, numResultsValue) } customCondition?.let { builder.field(CUSTOM_CONDITION_FIELD, customCondition) } builder.endObject() - builder.endObject() return builder } @@ -194,17 +193,6 @@ data class PPLTrigger( /* parse */ XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) // outer trigger object start - XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) // ppl_trigger field name - val triggerType = xcp.currentName() - if (triggerType != PPL_TRIGGER_FIELD) { - throw IllegalStateException( - "when parsing PPLMonitor, expected trigger to be of type $PPL_TRIGGER_FIELD " + - "but instead got \"$triggerType\"" - ) - } - - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) // inner trigger object start - while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { val fieldName = xcp.currentName() xcp.nextToken() @@ -281,8 +269,6 @@ data class PPLTrigger( } } - XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, xcp.nextToken(), xcp) // end of outer trigger object - /* validations */ requireNotNull(name) { "Trigger name must be included" } requireNotNull(severity) { "Trigger severity must be included" } @@ -318,5 +304,11 @@ data class PPLTrigger( customCondition ) } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): PPLTrigger { + return PPLTrigger(sin) + } } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt index f8910622..77f0b8e9 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -48,14 +48,5 @@ interface TriggerV2 : BaseModel { const val LAST_TRIGGERED_FIELD = "last_triggered_time" const val EXPIRE_FIELD = "expires" const val ACTIONS_FIELD = "actions" - - @JvmStatic - @Throws(IOException::class) - fun readFrom(sin: StreamInput): TriggerV2 { - return when (val type = sin.readEnum(TriggerV2Type::class.java)) { - TriggerV2Type.PPL_TRIGGER -> PPLTrigger(sin) - else -> throw IllegalStateException("Unexpected input \"$type\" when reading TriggerV2") - } - } } } From d836b0a9eea8d713e62143328a3a45f8abd8e8a3 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sat, 6 Sep 2025 11:57:46 -0700 Subject: [PATCH 20/30] adding validations for suppress durations --- .../commons/alerting/model/PPLMonitor.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index ebc98d63..c769c818 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -261,9 +261,6 @@ data class PPLMonitor( /* validations */ - // TODO: add validations for throttle actions time range - // (see alerting's TransportIndexMonitorAction.validateActionThrottle) - // ensure MonitorV2 XContent being parsed by PPLMonitor class is PPL Monitor type if (monitorType != PPL_MONITOR_TYPE) { throw IllegalArgumentException("Invalid monitor type: $monitorType") @@ -274,6 +271,21 @@ data class PPLMonitor( throw IllegalArgumentException("Monitor must include at least 1 trigger") } + // ensure the trigger suppress durations are valid + triggers.forEach { trigger -> + trigger.suppressDuration?.let { suppressDuration -> + // TODO: these max and min values are completely arbitrary, make them settings + val minValue = TimeValue.timeValueMinutes(1) + val maxValue = TimeValue.timeValueDays(5) + + require(suppressDuration <= maxValue) + { "Suppress duration must be at most $maxValue but was $suppressDuration" } + + require(suppressDuration >= minValue) + { "Suppress duration must be at least $minValue but was $suppressDuration" } + } + } + // if enabled, set time of MonitorV2 creation/update is set as enable time if (enabled && enabledTime == null) { enabledTime = Instant.now() From a911dad8b96551bd850d7dfb8b4d711de65293f5 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 06:23:16 -0700 Subject: [PATCH 21/30] removing action execution results from alertV2 and other cleanup --- .../opensearch/commons/alerting/model/AlertV2.kt | 14 +++++++++----- .../commons/alerting/model/MonitorV2RunResult.kt | 5 ----- .../commons/alerting/model/PPLMonitor.kt | 8 ++++---- .../commons/alerting/model/PPLMonitorRunResult.kt | 15 ++------------- .../commons/alerting/model/TriggerV2.kt | 1 + 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index d4022e2c..d77c4a6c 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -35,6 +35,7 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken import java.io.IOException import java.time.Instant +import org.opensearch.commons.alerting.util.nonOptionalTimeField data class AlertV2( val id: String = NO_ID, @@ -47,10 +48,10 @@ data class AlertV2( val triggerId: String, val triggerName: String, val queryResults: Map, + val triggeredTime: Instant, val expirationTime: Instant?, val errorMessage: String? = null, val severity: String, - val actionExecutionResults: List, val executionId: String? = null ) : Writeable, ToXContent { @Throws(IOException::class) @@ -69,10 +70,10 @@ data class AlertV2( triggerId = sin.readString(), triggerName = sin.readString(), queryResults = sin.readMap()!!.toMap(), + triggeredTime = sin.readInstant(), expirationTime = sin.readOptionalInstant(), errorMessage = sin.readOptionalString(), severity = sin.readString(), - actionExecutionResults = sin.readList(::ActionExecutionResult), executionId = sin.readOptionalString() ) @@ -89,10 +90,10 @@ data class AlertV2( out.writeString(triggerId) out.writeString(triggerName) out.writeMap(queryResults) + out.writeInstant(triggeredTime) out.writeOptionalInstant(expirationTime) out.writeOptionalString(errorMessage) out.writeString(severity) - out.writeCollection(actionExecutionResults) out.writeOptionalString(executionId) } @@ -110,7 +111,7 @@ data class AlertV2( .field(QUERY_RESULTS_FIELD, queryResults) .field(ERROR_MESSAGE_FIELD, errorMessage) .field(SEVERITY_FIELD, severity) - .field(ACTION_EXECUTION_RESULTS_FIELD, actionExecutionResults.toTypedArray()) + .nonOptionalTimeField(TRIGGERED_TIME_FIELD, triggeredTime) .optionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) .endObject() @@ -133,6 +134,7 @@ data class AlertV2( } companion object { + const val TRIGGERED_TIME_FIELD = "triggered_time" const val EXPIRATION_TIME_FIELD = "expiration_time" const val QUERY_RESULTS_FIELD = "query_results" @@ -149,6 +151,7 @@ data class AlertV2( lateinit var triggerName: String var queryResults: Map = mapOf() lateinit var severity: String + var triggeredTime: Instant? = null var expirationTime: Instant? = null var errorMessage: String? = null var executionId: String? = null @@ -173,6 +176,7 @@ data class AlertV2( TRIGGER_ID_FIELD -> triggerId = xcp.text() TRIGGER_NAME_FIELD -> triggerName = 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() @@ -197,10 +201,10 @@ data class AlertV2( triggerId = requireNotNull(triggerId), triggerName = requireNotNull(triggerName), queryResults = requireNotNull(queryResults), + triggeredTime = requireNotNull(triggeredTime), expirationTime = expirationTime, errorMessage = errorMessage, severity = severity, - actionExecutionResults = actionExecutionResults, executionId = executionId ) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt index be21feca..9ebfd32c 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt @@ -60,10 +60,5 @@ interface MonitorV2RunResult : Writeab } } } - - @Suppress("UNCHECKED_CAST") - fun suppressWarning(map: MutableMap?): Map { - return map as Map - } } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index c769c818..1d137726 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -32,10 +32,10 @@ import java.time.Instant private val logger = LogManager.getLogger(PPLMonitor::class.java) // TODO: probably change this to be called PPLSQLMonitor. A PPL Monitor and SQL Monitor -// TODO: would have the exact same functionality, except the choice of language -// TODO: when calling PPL/SQL plugin's execute API would be different. -// TODO: we dont need 2 different monitor types for that, just a simple if check -// TODO: for query language at monitor execution time +// 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 data class PPLMonitor( override val id: String = NO_ID, override val version: Long = NO_VERSION, diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt index 0cbcea0d..afd5e5bc 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt @@ -1,12 +1,10 @@ package org.opensearch.commons.alerting.model -import org.opensearch.commons.alerting.alerts.AlertError import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.ERROR_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.MONITOR_NAME_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_END_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_START_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.TRIGGER_RESULTS_FIELD -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.suppressWarning import org.opensearch.commons.alerting.util.nonOptionalTimeField import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput @@ -31,8 +29,8 @@ data class PPLMonitorRunResult( sin.readException(), // error sin.readInstant(), // periodStart sin.readInstant(), // periodEnd - suppressWarning(sin.readMap()) as Map, // triggerResults - sin.readMap() as Map> // pplQueryResults // TODO: if this works, delete suppressWarning call above + sin.readMap() as Map, // triggerResults + sin.readMap() as Map> // pplQueryResults ) override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { @@ -57,15 +55,6 @@ data class PPLMonitorRunResult( out.writeMap(pplQueryResults) } - // TODO: does this need any PPLMonitor specific logic, or can this just be deleted - override fun alertError(): AlertError? { - if (error != null) { - return AlertError(Instant.now(), "Failed running monitor:\n${error.userErrorMessage()}") - } - - return null - } - companion object { const val PPL_QUERY_RESULTS_FIELD = "ppl_query_results" } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt index 77f0b8e9..cd603e6a 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -28,6 +28,7 @@ interface TriggerV2 : BaseModel { enum class Severity(val value: String) { INFO("info"), + ERROR("error"), LOW("low"), MEDIUM("medium"), HIGH("high"), From 3c3ccef89fa2a90b9571bcf255a7f8023906956a Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 08:25:04 -0700 Subject: [PATCH 22/30] adding Execute Monitor actions to common utils --- .../alerting/action/AlertingActions.kt | 5 ++ .../action/ExecuteMonitorV2Request.kt | 69 +++++++++++++++++++ .../action/ExecuteMonitorV2Response.kt | 33 +++++++++ 3 files changed, 107 insertions(+) create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt create mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt index b985c5d6..4e7d7621 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt @@ -31,6 +31,7 @@ object AlertingActions { const val GET_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/get" const val SEARCH_MONITORS_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/search" const val DELETE_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/delete" + const val EXECUTE_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/execute" @JvmField val INDEX_MONITOR_ACTION_TYPE = @@ -111,4 +112,8 @@ object AlertingActions { @JvmField val DELETE_MONITOR_V2_ACTION_TYPE = ActionType(DELETE_MONITOR_V2_ACTION_NAME, ::DeleteMonitorV2Response) + + @JvmField + val EXECUTE_MONITOR_V2_ACTION_TYPE = + ActionType(EXECUTE_MONITOR_V2_ACTION_NAME, ::ExecuteMonitorV2Response) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt new file mode 100644 index 00000000..e9e6f1f4 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt @@ -0,0 +1,69 @@ +package org.opensearch.commons.alerting.action + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.action.ValidateActions +import org.opensearch.common.unit.TimeValue +import org.opensearch.commons.alerting.model.MonitorV2 +import org.opensearch.core.common.io.stream.StreamInput +import org.opensearch.core.common.io.stream.StreamOutput +import java.io.IOException + +class ExecuteMonitorV2Request : ActionRequest { + val dryrun: Boolean + val monitorId: String? // exactly one of monitorId or monitor must be non-null + val monitorV2: MonitorV2? + val requestStart: TimeValue? + val requestEnd: TimeValue + + constructor( + dryrun: Boolean, + monitorId: String?, + monitorV2: MonitorV2?, + requestStart: TimeValue? = null, + requestEnd: TimeValue, + ) : super() { + this.dryrun = dryrun + this.monitorId = monitorId + this.monitorV2 = monitorV2 + this.requestStart = requestStart + this.requestEnd = requestEnd + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + sin.readBoolean(), // dryrun + sin.readOptionalString(), // monitorId + if (sin.readBoolean()) { + MonitorV2.readFrom(sin) // monitor + } else null, + sin.readOptionalTimeValue(), + sin.readTimeValue(), // requestEnd + ) + + override fun validate(): ActionRequestValidationException? { + // ensure exactly one of monitor ID or monitorV2 is supplied + var exception: ActionRequestValidationException? = null + if (monitorV2 == null && monitorId == null) { + exception = ValidateActions.addValidationError( + "Neither a monitor ID or monitor object was supplied", + exception + ) + } + return exception + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + out.writeBoolean(dryrun) + out.writeOptionalString(monitorId) + if (monitorV2 != null) { + out.writeBoolean(true) + monitorV2.writeTo(out) + } else { + out.writeBoolean(false) + } + out.writeOptionalTimeValue(requestStart) + out.writeTimeValue(requestEnd) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt new file mode 100644 index 00000000..3ec237a1 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt @@ -0,0 +1,33 @@ +package org.opensearch.commons.alerting.action + +import org.opensearch.commons.alerting.model.MonitorV2RunResult +import org.opensearch.core.action.ActionResponse +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.ToXContentObject +import org.opensearch.core.xcontent.XContentBuilder +import java.io.IOException + +class ExecuteMonitorV2Response : ActionResponse, ToXContentObject { + val monitorV2RunResult: MonitorV2RunResult<*> + + constructor(monitorV2RunResult: MonitorV2RunResult<*>) : super() { + this.monitorV2RunResult = monitorV2RunResult + } + + @Throws(IOException::class) + constructor(sin: StreamInput) : this( + MonitorV2RunResult.readFrom(sin) // monitorRunResult + ) + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + monitorV2RunResult.writeTo(out) + } + + @Throws(IOException::class) + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + return monitorV2RunResult.toXContent(builder, ToXContent.EMPTY_PARAMS) + } +} \ No newline at end of file From 15bd211367f33f1f16eed1aa9dc8a3e992c93d8c Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 08:55:30 -0700 Subject: [PATCH 23/30] removing action execution results parsing logic --- .../org/opensearch/commons/alerting/model/AlertV2.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index d77c4a6c..6965afa8 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -155,7 +155,6 @@ data class AlertV2( var expirationTime: Instant? = null var errorMessage: String? = null var executionId: String? = null - val actionExecutionResults: MutableList = mutableListOf() ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { @@ -181,12 +180,6 @@ data class AlertV2( ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull() EXECUTION_ID_FIELD -> executionId = xcp.textOrNull() SEVERITY_FIELD -> severity = xcp.text() - ACTION_EXECUTION_RESULTS_FIELD -> { - ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp) - while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { - actionExecutionResults.add(ActionExecutionResult.parse(xcp)) - } - } } } From ef5ecd2ea89dc885e42e5aed9675e7427e71964f Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 09:00:03 -0700 Subject: [PATCH 24/30] AlertV2 linter issues --- .../opensearch/commons/alerting/model/AlertV2.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index 6965afa8..5e0030ad 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -1,16 +1,10 @@ package org.opensearch.commons.alerting.model import org.opensearch.common.lucene.uid.Versions -import org.opensearch.commons.alerting.alerts.AlertError -import org.opensearch.commons.alerting.model.Alert.Companion.ACKNOWLEDGED_TIME_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.ACTION_EXECUTION_RESULTS_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_HISTORY_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_ID_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_VERSION_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.END_TIME_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.ERROR_MESSAGE_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.EXECUTION_ID_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.LAST_NOTIFICATION_TIME_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_ID_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_NAME_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_VERSION_FIELD @@ -18,13 +12,11 @@ import org.opensearch.commons.alerting.model.Alert.Companion.NO_ID import org.opensearch.commons.alerting.model.Alert.Companion.NO_VERSION import org.opensearch.commons.alerting.model.Alert.Companion.SCHEMA_VERSION_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.SEVERITY_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.START_TIME_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.STATE_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_ID_FIELD import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_NAME_FIELD -import org.opensearch.commons.alerting.model.Alert.State import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION import org.opensearch.commons.alerting.util.instant +import org.opensearch.commons.alerting.util.nonOptionalTimeField import org.opensearch.commons.alerting.util.optionalTimeField import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput @@ -35,7 +27,6 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken import java.io.IOException import java.time.Instant -import org.opensearch.commons.alerting.util.nonOptionalTimeField data class AlertV2( val id: String = NO_ID, @@ -129,7 +120,7 @@ data class AlertV2( ERROR_MESSAGE_FIELD to errorMessage, EXECUTION_ID_FIELD to executionId, EXPIRATION_TIME_FIELD to expirationTime?.toEpochMilli(), - SEVERITY_FIELD to severity, + SEVERITY_FIELD to severity ) } From 2efc11564870ab114ac617f288ed8c568e5ef058 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 09:05:12 -0700 Subject: [PATCH 25/30] linter issues --- .../commons/alerting/action/ExecuteMonitorV2Request.kt | 10 ++++++---- .../alerting/action/ExecuteMonitorV2Response.kt | 2 +- .../opensearch/commons/alerting/model/PPLMonitor.kt | 6 ++---- .../opensearch/commons/alerting/model/PPLTrigger.kt | 1 - .../org/opensearch/commons/alerting/model/TriggerV2.kt | 2 -- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt index e9e6f1f4..7a825d15 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt @@ -21,7 +21,7 @@ class ExecuteMonitorV2Request : ActionRequest { monitorId: String?, monitorV2: MonitorV2?, requestStart: TimeValue? = null, - requestEnd: TimeValue, + requestEnd: TimeValue ) : super() { this.dryrun = dryrun this.monitorId = monitorId @@ -36,9 +36,11 @@ class ExecuteMonitorV2Request : ActionRequest { sin.readOptionalString(), // monitorId if (sin.readBoolean()) { MonitorV2.readFrom(sin) // monitor - } else null, + } else { + null + }, sin.readOptionalTimeValue(), - sin.readTimeValue(), // requestEnd + sin.readTimeValue() // requestEnd ) override fun validate(): ActionRequestValidationException? { @@ -66,4 +68,4 @@ class ExecuteMonitorV2Request : ActionRequest { out.writeOptionalTimeValue(requestStart) out.writeTimeValue(requestEnd) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt index 3ec237a1..4437c47e 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt @@ -30,4 +30,4 @@ class ExecuteMonitorV2Response : ActionResponse, ToXContentObject { override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { return monitorV2RunResult.toXContent(builder, ToXContent.EMPTY_PARAMS) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index 1d137726..09a32009 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -278,11 +278,9 @@ data class PPLMonitor( val minValue = TimeValue.timeValueMinutes(1) val maxValue = TimeValue.timeValueDays(5) - require(suppressDuration <= maxValue) - { "Suppress duration must be at most $maxValue but was $suppressDuration" } + require(suppressDuration <= maxValue) { "Suppress duration must be at most $maxValue but was $suppressDuration" } - require(suppressDuration >= minValue) - { "Suppress duration must be at least $minValue but was $suppressDuration" } + require(suppressDuration >= minValue) { "Suppress duration must be at least $minValue but was $suppressDuration" } } } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 62d82a45..0e5d5d41 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -25,7 +25,6 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils import java.io.IOException import java.time.Instant -import org.opensearch.commons.alerting.model.TriggerV2.TriggerV2Type private val logger = LogManager.getLogger(PPLTrigger::class.java) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt index cd603e6a..a6cb9e27 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt @@ -4,8 +4,6 @@ import org.opensearch.common.unit.TimeValue import org.opensearch.commons.alerting.model.PPLTrigger.Companion.PPL_TRIGGER_FIELD import org.opensearch.commons.alerting.model.action.Action import org.opensearch.commons.notifications.model.BaseModel -import org.opensearch.core.common.io.stream.StreamInput -import java.io.IOException import java.time.Instant interface TriggerV2 : BaseModel { From 6f9ea57b0cd3ad5b378a0d002f67bd514ec0e501 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 09:29:29 -0700 Subject: [PATCH 26/30] minor updates --- .../alerting/action/ExecuteMonitorV2Request.kt | 13 ++++--------- .../commons/alerting/model/MonitorV2RunResult.kt | 10 ---------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt index 7a825d15..15fc3b9f 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt @@ -43,17 +43,12 @@ class ExecuteMonitorV2Request : ActionRequest { sin.readTimeValue() // requestEnd ) - override fun validate(): ActionRequestValidationException? { - // ensure exactly one of monitor ID or monitorV2 is supplied - var exception: ActionRequestValidationException? = null + override fun validate(): ActionRequestValidationException? = if (monitorV2 == null && monitorId == null) { - exception = ValidateActions.addValidationError( - "Neither a monitor ID or monitor object was supplied", - exception - ) + ValidateActions.addValidationError("Neither a monitor ID nor monitor object was supplied", null) + } else { + null } - return exception - } @Throws(IOException::class) override fun writeTo(out: StreamOutput) { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt index 9ebfd32c..dfb7a420 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt @@ -16,16 +16,6 @@ interface MonitorV2RunResult : Writeab enum class MonitorV2RunResultType() { PPL_MONITOR_RUN_RESULT; - -// override fun toString(): String { -// return value -// } - -// companion object { -// fun enumFromString(value: String): MonitorV2Type? { -// return MonitorV2Type.entries.find { it.value == value } -// } -// } } /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ From 4443472f061039ace38be37d07873cac319809a9 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 8 Sep 2025 09:54:53 -0700 Subject: [PATCH 27/30] misc cleanup --- .../alerting/model/MonitorV2RunResult.kt | 11 +--------- .../alerting/model/PPLMonitorRunResult.kt | 4 ++-- .../alerting/model/PPLTriggerRunResult.kt | 20 +------------------ .../alerting/model/TriggerV2RunResult.kt | 8 -------- 4 files changed, 4 insertions(+), 39 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt index dfb7a420..99a42472 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt @@ -18,17 +18,8 @@ interface MonitorV2RunResult : Writeab PPL_MONITOR_RUN_RESULT; } - /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ - fun alertError(): AlertError? { - if (error != null) { - return AlertError(Instant.now(), "Failed running monitor:\n${error!!.userErrorMessage()}") - } - - return null - } - companion object { - const val MONITOR_NAME_FIELD = "monitor_name" + const val MONITOR_V2_NAME_FIELD = "monitor_v2_name" const val ERROR_FIELD = "error" const val PERIOD_START_FIELD = "period_start" const val PERIOD_END_FIELD = "period_end" diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt index afd5e5bc..91c89eea 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt @@ -1,7 +1,7 @@ package org.opensearch.commons.alerting.model import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.ERROR_FIELD -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.MONITOR_NAME_FIELD +import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.MONITOR_V2_NAME_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_END_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_START_FIELD import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.TRIGGER_RESULTS_FIELD @@ -35,7 +35,7 @@ data class PPLMonitorRunResult( override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { builder.startObject() - builder.field(MONITOR_NAME_FIELD, monitorName) + builder.field(MONITOR_V2_NAME_FIELD, monitorName) builder.nonOptionalTimeField(PERIOD_START_FIELD, periodStart) builder.nonOptionalTimeField(PERIOD_END_FIELD, periodEnd) builder.field(ERROR_FIELD, error?.message) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt index d9de4813..1723d04f 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt @@ -15,7 +15,6 @@ data class PPLTriggerRunResult( override var triggerName: String, override var triggered: Boolean, override var error: Exception?, - var actionResults: MutableMap = mutableMapOf() ) : TriggerV2RunResult { @Throws(IOException::class) @@ -23,8 +22,7 @@ data class PPLTriggerRunResult( constructor(sin: StreamInput) : this( triggerName = sin.readString(), triggered = sin.readBoolean(), - error = sin.readException(), - actionResults = sin.readMap() as MutableMap + error = sin.readException() ) override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { @@ -32,7 +30,6 @@ data class PPLTriggerRunResult( builder.field(NAME_FIELD, triggerName) builder.field(TRIGGERED_FIELD, triggered) builder.field(ERROR_FIELD, error?.message) - builder.field(ACTION_RESULTS_FIELD, actionResults as Map) builder.endObject() return builder } @@ -42,24 +39,9 @@ data class PPLTriggerRunResult( out.writeString(triggerName) out.writeBoolean(triggered) out.writeException(error) - out.writeMap(actionResults as Map) - } - - override fun alertError(): AlertError? { - if (error != null) { - return AlertError(Instant.now(), "Failed evaluating trigger:\n${error!!.userErrorMessage()}") - } - for (actionResult in actionResults.values) { - if (actionResult.error != null) { - return AlertError(Instant.now(), "Failed running action:\n${actionResult.error.userErrorMessage()}") - } - } - return null } companion object { - const val ACTION_RESULTS_FIELD = "action_results" - @JvmStatic @Throws(IOException::class) fun readFrom(sin: StreamInput): TriggerRunResult { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt index d70809db..0df411bc 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt @@ -11,14 +11,6 @@ interface TriggerV2RunResult : Writeable, ToXContent { val triggered: Boolean val error: Exception? - /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ - fun alertError(): AlertError? { - if (error != null) { - return AlertError(Instant.now(), "Failed evaluating trigger:\n${error!!.userErrorMessage()}") - } - return null - } - companion object { const val NAME_FIELD = "name" const val TRIGGERED_FIELD = "triggered" From 5750e7097205fc4167f592204b66f475da87c7a0 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 10 Sep 2025 09:28:59 -0700 Subject: [PATCH 28/30] adding javadocs --- .../commons/alerting/model/AlertV2.kt | 58 +++++++++++++++---- .../commons/alerting/model/PPLMonitor.kt | 17 ++++++ .../commons/alerting/model/PPLTrigger.kt | 51 ++++++++++++---- .../alerting/model/TriggerV2RunResult.kt | 2 - 4 files changed, 103 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt index 5e0030ad..ab0f4eaa 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt @@ -17,7 +17,6 @@ import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_NAME_FIELD import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION import org.opensearch.commons.alerting.util.instant import org.opensearch.commons.alerting.util.nonOptionalTimeField -import org.opensearch.commons.alerting.util.optionalTimeField import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.common.io.stream.StreamOutput import org.opensearch.core.common.io.stream.Writeable @@ -27,7 +26,37 @@ import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken import java.io.IOException import java.time.Instant +import org.opensearch.commons.alerting.model.TriggerV2.Severity +/** + * 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 specific Trigger that generated this alert. + * @property triggerName Name of the trigger that generated this alert. + * @property queryResults Results from the Monitor's query that caused the Trigger to fire. + * Stored as a map of field names to their values. + * @property triggeredTime Timestamp when the Alert was generated. + * @property expirationTime Timestamp when the Alert should be considered 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. Created when a TriggerV2's condition is met. The TriggerV2 fires and forgets the Alert. + * 2. Stored in the alerts index. AlertV2s are stateless. (e.g. they are never ACTIVE or COMPLETED) + * 3. Alert is permanently deleted at [expirationTime] + */ data class AlertV2( val id: String = NO_ID, val version: Long = NO_VERSION, @@ -40,9 +69,9 @@ data class AlertV2( val triggerName: String, val queryResults: Map, val triggeredTime: Instant, - val expirationTime: Instant?, + val expirationTime: Instant, val errorMessage: String? = null, - val severity: String, + val severity: Severity, val executionId: String? = null ) : Writeable, ToXContent { @Throws(IOException::class) @@ -62,9 +91,9 @@ data class AlertV2( triggerName = sin.readString(), queryResults = sin.readMap()!!.toMap(), triggeredTime = sin.readInstant(), - expirationTime = sin.readOptionalInstant(), + expirationTime = sin.readInstant(), errorMessage = sin.readOptionalString(), - severity = sin.readString(), + severity = sin.readEnum(Severity::class.java), executionId = sin.readOptionalString() ) @@ -82,9 +111,9 @@ data class AlertV2( out.writeString(triggerName) out.writeMap(queryResults) out.writeInstant(triggeredTime) - out.writeOptionalInstant(expirationTime) + out.writeInstant(expirationTime) out.writeOptionalString(errorMessage) - out.writeString(severity) + out.writeEnum(severity) out.writeOptionalString(executionId) } @@ -101,9 +130,9 @@ data class AlertV2( .field(TRIGGER_NAME_FIELD, triggerName) .field(QUERY_RESULTS_FIELD, queryResults) .field(ERROR_MESSAGE_FIELD, errorMessage) - .field(SEVERITY_FIELD, severity) + .field(SEVERITY_FIELD, severity.value) .nonOptionalTimeField(TRIGGERED_TIME_FIELD, triggeredTime) - .optionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) + .nonOptionalTimeField(EXPIRATION_TIME_FIELD, expirationTime) .endObject() // if (!secure) { @@ -141,7 +170,7 @@ data class AlertV2( lateinit var triggerId: String lateinit var triggerName: String var queryResults: Map = mapOf() - lateinit var severity: String + lateinit var severity: Severity var triggeredTime: Instant? = null var expirationTime: Instant? = null var errorMessage: String? = null @@ -170,7 +199,12 @@ data class AlertV2( EXPIRATION_TIME_FIELD -> expirationTime = xcp.instant() ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull() EXECUTION_ID_FIELD -> executionId = xcp.textOrNull() - SEVERITY_FIELD -> severity = xcp.text() + TriggerV2.SEVERITY_FIELD -> { + val input = xcp.text() + val enumMatchResult = Severity.enumFromString(input) + ?: throw IllegalStateException("Invalid value for ${TriggerV2.SEVERITY_FIELD}: $input. Supported values are ${Severity.entries.map { it.value }}") + severity = enumMatchResult + } } } @@ -186,7 +220,7 @@ data class AlertV2( triggerName = requireNotNull(triggerName), queryResults = requireNotNull(queryResults), triggeredTime = requireNotNull(triggeredTime), - expirationTime = expirationTime, + expirationTime = requireNotNull(expirationTime), errorMessage = errorMessage, severity = severity, executionId = executionId diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt index 09a32009..601e11e3 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt @@ -36,6 +36,23 @@ private val logger = LogManager.getLogger(PPLMonitor::class.java) // 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. + * Only applicable if Monitor uses CRON schedule. Optional even if CRON schedule is used. + * @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, diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt index 0e5d5d41..eac6cc89 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt @@ -28,12 +28,45 @@ import java.time.Instant private val logger = LogManager.getLogger(PPLTrigger::class.java) +/** + * The PPL Trigger for PPL Monitors + * + * There are two types of PPLTrigger conditions: NUMBER_OF_RESULT and CUSTOM + * NUMBER_OF_RESULTS: triggers based on if the number of query results returned by the PPLMonitor + * query meets some threshold + * CUSTOM: triggers based on a custom condition that user specifies + * This trigger can operate in either result set or per-result mode and supports + * both numeric result conditions and custom conditions. + * + * 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 suppressDuration Optional duration for which alerts from this Trigger should be suppressed. + * Null indicates no suppression. + * @property expireDuration Duration 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, + * null otherwise. + * @property numResultsValue The threshold value for NUMBER_OF_RESULTS conditions. Required if using NUMBER_OF_RESULTS conditions, + * null otherwise. + * @property customCondition A custom condition expression. Required if using CUSTOM conditions, + * null otherwise. + */ data class PPLTrigger( override val id: String = UUIDs.base64UUID(), override val name: String, override val severity: Severity, override val suppressDuration: TimeValue?, - override val expireDuration: TimeValue?, + override val expireDuration: TimeValue, override var lastTriggeredTime: Instant?, override val actions: List, val mode: TriggerMode, // result_set or per_result @@ -50,7 +83,7 @@ data class PPLTrigger( sin.readEnum(Severity::class.java), // severity // parseTimeValue() is typically used to parse OpenSearch settings // the second param is supposed to accept a setting name, but here we're passing in our own name - TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_SUPPRESS_SETTING_NAME), // suppressDuration + TimeValue.parseTimeValue(sin.readOptionalString(), PLACEHOLDER_SUPPRESS_SETTING_NAME), // suppressDuration TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_EXPIRE_SETTING_NAME), // expireDuration sin.readOptionalInstant(), // lastTriggeredTime sin.readList(::Action), // actions @@ -70,9 +103,7 @@ data class PPLTrigger( out.writeBoolean(suppressDuration != null) suppressDuration?.let { out.writeString(suppressDuration.toHumanReadableString(0)) } - out.writeBoolean(expireDuration != null) - expireDuration?.let { out.writeString(expireDuration.toHumanReadableString(0)) } - + out.writeString(expireDuration.toHumanReadableString(0)) out.writeOptionalInstant(lastTriggeredTime) out.writeCollection(actions) out.writeEnum(mode) @@ -91,7 +122,7 @@ data class PPLTrigger( builder.field(NAME_FIELD, name) builder.field(SEVERITY_FIELD, severity.value) builder.field(SUPPRESS_FIELD, suppressDuration?.toHumanReadableString(0)) - builder.field(EXPIRE_FIELD, expireDuration?.toHumanReadableString(0)) + builder.field(EXPIRE_FIELD, expireDuration.toHumanReadableString(0)) builder.optionalTimeField(LAST_TRIGGERED_FIELD, lastTriggeredTime) builder.field(ACTIONS_FIELD, actions.toTypedArray()) builder.field(MODE_FIELD, mode.value) @@ -180,7 +211,7 @@ data class PPLTrigger( var name: String? = null var severity: Severity? = null var suppressDuration: TimeValue? = null - var expireDuration: TimeValue? = null + var expireDuration: TimeValue = TimeValue.timeValueDays(7) // default to 7 days // TODO: add this as a setting var lastTriggeredTime: Instant? = null val actions: MutableList = mutableListOf() var mode: TriggerMode? = null @@ -250,11 +281,9 @@ data class PPLTrigger( } } EXPIRE_FIELD -> { - expireDuration = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { + if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { // if expire field is null, skip reading it and let it retain the default value val input = xcp.text() - TimeValue.parseTimeValue(input, PLACEHOLDER_EXPIRE_SETTING_NAME) // throws IllegalArgumentException if there's parsing error + expireDuration = TimeValue.parseTimeValue(input, PLACEHOLDER_EXPIRE_SETTING_NAME) // throws IllegalArgumentException if there's parsing error } } LAST_TRIGGERED_FIELD -> lastTriggeredTime = xcp.instant() diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt index 0df411bc..3e1796b4 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt @@ -1,9 +1,7 @@ package org.opensearch.commons.alerting.model -import org.opensearch.commons.alerting.alerts.AlertError import org.opensearch.core.common.io.stream.Writeable import org.opensearch.core.xcontent.ToXContent -import java.time.Instant interface TriggerV2RunResult : Writeable, ToXContent { From 5cb2537af4469c39a75de4143e2b7a669bb5ae3b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 10 Sep 2025 11:17:03 -0700 Subject: [PATCH 29/30] deleting models here to put them in alerting core --- .../alerting/action/AlertingActions.kt | 28 -- .../alerting/action/DeleteMonitorV2Request.kt | 34 -- .../action/DeleteMonitorV2Response.kt | 38 -- .../action/ExecuteMonitorV2Request.kt | 66 ---- .../action/ExecuteMonitorV2Response.kt | 33 -- .../alerting/action/GetMonitorV2Request.kt | 47 --- .../alerting/action/GetMonitorV2Response.kt | 75 ---- .../alerting/action/IndexMonitorV2Request.kt | 64 ---- .../alerting/action/IndexMonitorV2Response.kt | 68 ---- .../alerting/action/SearchMonitorV2Request.kt | 32 -- .../commons/alerting/model/AlertV2.kt | 236 ------------ .../commons/alerting/model/MonitorV2.kt | 102 ------ .../alerting/model/MonitorV2RunResult.kt | 45 --- .../commons/alerting/model/PPLMonitor.kt | 345 ------------------ .../alerting/model/PPLMonitorRunResult.kt | 61 ---- .../commons/alerting/model/PPLTrigger.kt | 342 ----------------- .../alerting/model/PPLTriggerRunResult.kt | 51 --- .../commons/alerting/model/TriggerV2.kt | 51 --- .../alerting/model/TriggerV2RunResult.kt | 17 - .../commons/ppl/PPLPluginInterface.kt | 50 --- 20 files changed, 1785 deletions(-) delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt delete mode 100644 src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt index 4e7d7621..fcf98261 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/action/AlertingActions.kt @@ -8,7 +8,6 @@ import org.opensearch.action.ActionType import org.opensearch.action.search.SearchResponse object AlertingActions { - // Alerting V1 const val INDEX_MONITOR_ACTION_NAME = "cluster:admin/opendistro/alerting/monitor/write" const val INDEX_WORKFLOW_ACTION_NAME = "cluster:admin/opensearch/alerting/workflow/write" const val GET_ALERTS_ACTION_NAME = "cluster:admin/opendistro/alerting/alerts/get" @@ -26,13 +25,6 @@ object AlertingActions { const val SEARCH_COMMENTS_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/search" const val DELETE_COMMENT_ACTION_NAME = "cluster:admin/opensearch/alerting/comments/delete" - // Alerting V2 - const val INDEX_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/write" - const val GET_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/get" - const val SEARCH_MONITORS_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/search" - const val DELETE_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/delete" - const val EXECUTE_MONITOR_V2_ACTION_NAME = "cluster:admin/opensearch/alerting/v2/monitor/execute" - @JvmField val INDEX_MONITOR_ACTION_TYPE = ActionType(INDEX_MONITOR_ACTION_NAME, ::IndexMonitorResponse) @@ -96,24 +88,4 @@ object AlertingActions { @JvmField val DELETE_COMMENT_ACTION_TYPE = ActionType(DELETE_COMMENT_ACTION_NAME, ::DeleteCommentResponse) - - @JvmField - val INDEX_MONITOR_V2_ACTION_TYPE = - ActionType(INDEX_MONITOR_V2_ACTION_NAME, ::IndexMonitorV2Response) - - @JvmField - val GET_MONITOR_V2_ACTION_TYPE = - ActionType(GET_MONITOR_V2_ACTION_NAME, ::GetMonitorV2Response) - - @JvmField - val SEARCH_MONITORS_V2_ACTION_TYPE = - ActionType(SEARCH_MONITORS_V2_ACTION_NAME, ::SearchResponse) - - @JvmField - val DELETE_MONITOR_V2_ACTION_TYPE = - ActionType(DELETE_MONITOR_V2_ACTION_NAME, ::DeleteMonitorV2Response) - - @JvmField - val EXECUTE_MONITOR_V2_ACTION_TYPE = - ActionType(EXECUTE_MONITOR_V2_ACTION_NAME, ::ExecuteMonitorV2Response) } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt deleted file mode 100644 index 410ae250..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Request.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.action.ActionRequest -import org.opensearch.action.ActionRequestValidationException -import org.opensearch.action.support.WriteRequest -import org.opensearch.core.common.io.stream.StreamInput -import org.opensearch.core.common.io.stream.StreamOutput -import java.io.IOException - -class DeleteMonitorV2Request : ActionRequest { - val monitorV2Id: String - val refreshPolicy: WriteRequest.RefreshPolicy - - constructor(monitorV2Id: String, refreshPolicy: WriteRequest.RefreshPolicy) : super() { - this.monitorV2Id = monitorV2Id - this.refreshPolicy = refreshPolicy - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - monitorV2Id = sin.readString(), - refreshPolicy = WriteRequest.RefreshPolicy.readFrom(sin) - ) - - override fun validate(): ActionRequestValidationException? { - return null - } - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeString(monitorV2Id) - refreshPolicy.writeTo(out) - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt deleted file mode 100644 index 6d7ffef0..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorV2Response.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.commons.alerting.util.IndexUtils -import org.opensearch.commons.notifications.action.BaseResponse -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 - -class DeleteMonitorV2Response : BaseResponse { - var id: String - var version: Long - - constructor( - id: String, - version: Long - ) : super() { - this.id = id - this.version = version - } - - constructor(sin: StreamInput) : this( - sin.readString(), // id - sin.readLong() // version - ) - - override fun writeTo(out: StreamOutput) { - out.writeString(id) - out.writeLong(version) - } - - override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { - return builder.startObject() - .field(IndexUtils._ID, id) - .field(IndexUtils._VERSION, version) - .endObject() - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt deleted file mode 100644 index 15fc3b9f..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Request.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.action.ActionRequest -import org.opensearch.action.ActionRequestValidationException -import org.opensearch.action.ValidateActions -import org.opensearch.common.unit.TimeValue -import org.opensearch.commons.alerting.model.MonitorV2 -import org.opensearch.core.common.io.stream.StreamInput -import org.opensearch.core.common.io.stream.StreamOutput -import java.io.IOException - -class ExecuteMonitorV2Request : ActionRequest { - val dryrun: Boolean - val monitorId: String? // exactly one of monitorId or monitor must be non-null - val monitorV2: MonitorV2? - val requestStart: TimeValue? - val requestEnd: TimeValue - - constructor( - dryrun: Boolean, - monitorId: String?, - monitorV2: MonitorV2?, - requestStart: TimeValue? = null, - requestEnd: TimeValue - ) : super() { - this.dryrun = dryrun - this.monitorId = monitorId - this.monitorV2 = monitorV2 - this.requestStart = requestStart - this.requestEnd = requestEnd - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - sin.readBoolean(), // dryrun - sin.readOptionalString(), // monitorId - if (sin.readBoolean()) { - MonitorV2.readFrom(sin) // monitor - } else { - null - }, - sin.readOptionalTimeValue(), - sin.readTimeValue() // requestEnd - ) - - override fun validate(): ActionRequestValidationException? = - if (monitorV2 == null && monitorId == null) { - ValidateActions.addValidationError("Neither a monitor ID nor monitor object was supplied", null) - } else { - null - } - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeBoolean(dryrun) - out.writeOptionalString(monitorId) - if (monitorV2 != null) { - out.writeBoolean(true) - monitorV2.writeTo(out) - } else { - out.writeBoolean(false) - } - out.writeOptionalTimeValue(requestStart) - out.writeTimeValue(requestEnd) - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt deleted file mode 100644 index 4437c47e..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/ExecuteMonitorV2Response.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.commons.alerting.model.MonitorV2RunResult -import org.opensearch.core.action.ActionResponse -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.ToXContentObject -import org.opensearch.core.xcontent.XContentBuilder -import java.io.IOException - -class ExecuteMonitorV2Response : ActionResponse, ToXContentObject { - val monitorV2RunResult: MonitorV2RunResult<*> - - constructor(monitorV2RunResult: MonitorV2RunResult<*>) : super() { - this.monitorV2RunResult = monitorV2RunResult - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - MonitorV2RunResult.readFrom(sin) // monitorRunResult - ) - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - monitorV2RunResult.writeTo(out) - } - - @Throws(IOException::class) - override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { - return monitorV2RunResult.toXContent(builder, ToXContent.EMPTY_PARAMS) - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt deleted file mode 100644 index bd944a50..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Request.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.action.ActionRequest -import org.opensearch.action.ActionRequestValidationException -import org.opensearch.core.common.io.stream.StreamInput -import org.opensearch.core.common.io.stream.StreamOutput -import org.opensearch.search.fetch.subphase.FetchSourceContext -import java.io.IOException - -class GetMonitorV2Request : ActionRequest { - val monitorV2Id: String - val version: Long - val srcContext: FetchSourceContext? - - constructor( - monitorV2Id: String, - version: Long, - srcContext: FetchSourceContext? - ) : super() { - this.monitorV2Id = monitorV2Id - this.version = version - this.srcContext = srcContext - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - sin.readString(), // monitorV2Id - sin.readLong(), // version - if (sin.readBoolean()) { - FetchSourceContext(sin) // srcContext - } else { - null - } - ) - - override fun validate(): ActionRequestValidationException? { - return null - } - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeString(monitorV2Id) - out.writeLong(version) - out.writeBoolean(srcContext != null) - srcContext?.writeTo(out) - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt deleted file mode 100644 index 8232fda0..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorV2Response.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.commons.alerting.model.MonitorV2 -import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID -import org.opensearch.commons.alerting.util.IndexUtils.Companion._PRIMARY_TERM -import org.opensearch.commons.alerting.util.IndexUtils.Companion._SEQ_NO -import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION -import org.opensearch.commons.notifications.action.BaseResponse -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 - -class GetMonitorV2Response : BaseResponse { - var id: String - var version: Long - var seqNo: Long - var primaryTerm: Long - var monitorV2: MonitorV2? - - constructor( - id: String, - version: Long, - seqNo: Long, - primaryTerm: Long, - monitorV2: MonitorV2? - ) : super() { - this.id = id - this.version = version - this.seqNo = seqNo - this.primaryTerm = primaryTerm - this.monitorV2 = monitorV2 - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - id = sin.readString(), // id - version = sin.readLong(), // version - seqNo = sin.readLong(), // seqNo - primaryTerm = sin.readLong(), // primaryTerm - monitorV2 = if (sin.readBoolean()) { - MonitorV2.readFrom(sin) // monitorV2 - } else { - null - } - ) - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeString(id) - out.writeLong(version) - out.writeLong(seqNo) - out.writeLong(primaryTerm) - if (monitorV2 != null) { - out.writeBoolean(true) - monitorV2?.writeTo(out) - } else { - out.writeBoolean(false) - } - } - - @Throws(IOException::class) - override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { - builder.startObject() - .field(_ID, id) - .field(_VERSION, version) - .field(_SEQ_NO, seqNo) - .field(_PRIMARY_TERM, primaryTerm) - if (monitorV2 != null) { - builder.field("monitorV2", monitorV2) - } - return builder.endObject() - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt deleted file mode 100644 index 298372b7..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Request.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.action.ActionRequest -import org.opensearch.action.ActionRequestValidationException -import org.opensearch.action.support.WriteRequest -import org.opensearch.commons.alerting.model.MonitorV2 -import org.opensearch.core.common.io.stream.StreamInput -import org.opensearch.core.common.io.stream.StreamOutput -import org.opensearch.rest.RestRequest -import java.io.IOException - -class IndexMonitorV2Request : ActionRequest { - val monitorId: String - val seqNo: Long - val primaryTerm: Long - val refreshPolicy: WriteRequest.RefreshPolicy - val method: RestRequest.Method - var monitorV2: MonitorV2 -// val rbacRoles: List? - - constructor( - monitorId: String, - seqNo: Long, - primaryTerm: Long, - refreshPolicy: WriteRequest.RefreshPolicy, - method: RestRequest.Method, - monitorV2: MonitorV2 -// rbacRoles: List? = null - ) : super() { - this.monitorId = monitorId - this.seqNo = seqNo - this.primaryTerm = primaryTerm - this.refreshPolicy = refreshPolicy - this.method = method - this.monitorV2 = monitorV2 -// this.rbacRoles = rbacRoles - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - monitorId = sin.readString(), - seqNo = sin.readLong(), - primaryTerm = sin.readLong(), - refreshPolicy = WriteRequest.RefreshPolicy.readFrom(sin), - method = sin.readEnum(RestRequest.Method::class.java), - monitorV2 = MonitorV2.readFrom(sin) -// rbacRoles = sin.readOptionalStringList() - ) - - override fun validate(): ActionRequestValidationException? { - return null - } - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeString(monitorId) - out.writeLong(seqNo) - out.writeLong(primaryTerm) - refreshPolicy.writeTo(out) - out.writeEnum(method) - MonitorV2.writeTo(out, monitorV2) -// out.writeOptionalStringCollection(rbacRoles) - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt deleted file mode 100644 index c47f12ab..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorV2Response.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.commons.alerting.model.MonitorV2 -import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID -import org.opensearch.commons.alerting.util.IndexUtils.Companion._PRIMARY_TERM -import org.opensearch.commons.alerting.util.IndexUtils.Companion._SEQ_NO -import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION -import org.opensearch.commons.notifications.action.BaseResponse -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 - -class IndexMonitorV2Response : BaseResponse { - var id: String - var version: Long - var seqNo: Long - var primaryTerm: Long - var monitorV2: MonitorV2 - - constructor( - id: String, - version: Long, - seqNo: Long, - primaryTerm: Long, - monitorV2: MonitorV2 - ) : super() { - this.id = id - this.version = version - this.seqNo = seqNo - this.primaryTerm = primaryTerm - this.monitorV2 = monitorV2 - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - sin.readString(), // id - sin.readLong(), // version - sin.readLong(), // seqNo - sin.readLong(), // primaryTerm - MonitorV2.readFrom(sin) // monitorV2 - ) - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - out.writeString(id) - out.writeLong(version) - out.writeLong(seqNo) - out.writeLong(primaryTerm) - MonitorV2.writeTo(out, monitorV2) - } - - @Throws(IOException::class) - override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { - return builder.startObject() - .field(_ID, id) - .field(_VERSION, version) - .field(_SEQ_NO, seqNo) - .field(_PRIMARY_TERM, primaryTerm) - .field(MONITOR_V2_FIELD, monitorV2) - .endObject() - } - - companion object { - const val MONITOR_V2_FIELD = "monitor_v2" - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt b/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt deleted file mode 100644 index 12a2129f..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/action/SearchMonitorV2Request.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.opensearch.commons.alerting.action - -import org.opensearch.action.ActionRequest -import org.opensearch.action.ActionRequestValidationException -import org.opensearch.action.search.SearchRequest -import org.opensearch.core.common.io.stream.StreamInput -import org.opensearch.core.common.io.stream.StreamOutput -import java.io.IOException - -class SearchMonitorV2Request : ActionRequest { - val searchRequest: SearchRequest - - constructor( - searchRequest: SearchRequest - ) : super() { - this.searchRequest = searchRequest - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - searchRequest = SearchRequest(sin) - ) - - override fun validate(): ActionRequestValidationException? { - return null - } - - @Throws(IOException::class) - override fun writeTo(out: StreamOutput) { - searchRequest.writeTo(out) - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt deleted file mode 100644 index ab0f4eaa..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/AlertV2.kt +++ /dev/null @@ -1,236 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.opensearch.common.lucene.uid.Versions -import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_ID_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.ALERT_VERSION_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.ERROR_MESSAGE_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.EXECUTION_ID_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_ID_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_NAME_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.MONITOR_VERSION_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.NO_ID -import org.opensearch.commons.alerting.model.Alert.Companion.NO_VERSION -import org.opensearch.commons.alerting.model.Alert.Companion.SCHEMA_VERSION_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.SEVERITY_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_ID_FIELD -import org.opensearch.commons.alerting.model.Alert.Companion.TRIGGER_NAME_FIELD -import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION -import org.opensearch.commons.alerting.util.instant -import org.opensearch.commons.alerting.util.nonOptionalTimeField -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 -import org.opensearch.commons.alerting.model.TriggerV2.Severity - -/** - * 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 specific Trigger that generated this alert. - * @property triggerName Name of the trigger that generated this alert. - * @property queryResults Results from the Monitor's query that caused the Trigger to fire. - * Stored as a map of field names to their values. - * @property triggeredTime Timestamp when the Alert was generated. - * @property expirationTime Timestamp when the Alert should be considered 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. Created when a TriggerV2's condition is met. The TriggerV2 fires and forgets the Alert. - * 2. Stored in the alerts index. AlertV2s are stateless. (e.g. they are never ACTIVE or COMPLETED) - * 3. Alert is permanently deleted at [expirationTime] - */ -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 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(), - queryResults = sin.readMap()!!.toMap(), - 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.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 { - builder.startObject() - .field(ALERT_ID_FIELD, id) - .field(ALERT_VERSION_FIELD, version) - .field(MONITOR_ID_FIELD, monitorId) - .field(SCHEMA_VERSION_FIELD, schemaVersion) - .field(MONITOR_VERSION_FIELD, monitorVersion) - .field(MONITOR_NAME_FIELD, monitorName) - .field(EXECUTION_ID_FIELD, executionId) - .field(TRIGGER_ID_FIELD, triggerId) - .field(TRIGGER_NAME_FIELD, triggerName) - .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) - .endObject() - -// if (!secure) { -// builder.optionalUserField(MONITOR_USER_FIELD, monitorUser) -// } - - return builder - } - - fun asTemplateArg(): Map { - return mapOf( - ALERT_ID_FIELD to id, - ALERT_VERSION_FIELD to version, - ERROR_MESSAGE_FIELD to errorMessage, - EXECUTION_ID_FIELD to executionId, - EXPIRATION_TIME_FIELD to expirationTime?.toEpochMilli(), - SEVERITY_FIELD to severity - ) - } - - companion object { - const val TRIGGERED_TIME_FIELD = "triggered_time" - const val EXPIRATION_TIME_FIELD = "expiration_time" - const val QUERY_RESULTS_FIELD = "query_results" - - @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 - 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_ID_FIELD -> monitorId = xcp.text() - SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue() - MONITOR_NAME_FIELD -> monitorName = xcp.text() - MONITOR_VERSION_FIELD -> monitorVersion = xcp.longValue() -// MONITOR_USER_FIELD -> -// monitorUser = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { -// null -// } else { -// User.parse(xcp) -// } - TRIGGER_ID_FIELD -> triggerId = xcp.text() - TRIGGER_NAME_FIELD -> triggerName = 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 IllegalStateException("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), - 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/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt deleted file mode 100644 index 489e1624..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.opensearch.common.CheckedFunction -import org.opensearch.common.unit.TimeValue -import org.opensearch.commons.alerting.model.PPLMonitor.Companion.PPL_MONITOR_TYPE -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.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 triggers: List - val schemaVersion: Int // for updating monitors - val lookBackWindow: TimeValue? // how far back to look when querying data during monitor execution - - fun asTemplateArg(): Map - - 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 MONITOR_TYPE_FIELD = "monitor_type" - 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 TRIGGERS_FIELD = "triggers" - const val LOOK_BACK_WINDOW_FIELD = "look_back_window" - - // default values - const val NO_ID = "" - const val NO_VERSION = 1L - - 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(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) // outer monitor object start - - 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") - - 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(MonitorV2.MonitorV2Type.PPL_MONITOR) - monitorV2.writeTo(out) - } - } - } - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt deleted file mode 100644 index 99a42472..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/MonitorV2RunResult.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.opensearch.commons.alerting.alerts.AlertError -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 java.time.Instant - -interface MonitorV2RunResult : Writeable, ToXContent { - val monitorName: String - val error: Exception? - val periodStart: Instant - val periodEnd: Instant - val triggerResults: Map - - enum class MonitorV2RunResultType() { - PPL_MONITOR_RUN_RESULT; - } - - companion object { - const val MONITOR_V2_NAME_FIELD = "monitor_v2_name" - const val ERROR_FIELD = "error" - const val PERIOD_START_FIELD = "period_start" - const val PERIOD_END_FIELD = "period_end" - 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/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt deleted file mode 100644 index 601e11e3..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitor.kt +++ /dev/null @@ -1,345 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.apache.logging.log4j.LogManager -import org.opensearch.common.unit.TimeValue -import org.opensearch.commons.alerting.model.Monitor.Companion.SCHEMA_VERSION_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.ENABLED_TIME_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.LAST_UPDATE_TIME_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.LOOK_BACK_WINDOW_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_TYPE_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.MONITOR_V2_TYPE -import org.opensearch.commons.alerting.model.MonitorV2.Companion.NAME_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_ID -import org.opensearch.commons.alerting.model.MonitorV2.Companion.NO_VERSION -import org.opensearch.commons.alerting.model.MonitorV2.Companion.SCHEDULE_FIELD -import org.opensearch.commons.alerting.model.MonitorV2.Companion.TRIGGERS_FIELD -import org.opensearch.commons.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION -import org.opensearch.commons.alerting.util.IndexUtils.Companion._ID -import org.opensearch.commons.alerting.util.IndexUtils.Companion._VERSION -import org.opensearch.commons.alerting.util.instant -import org.opensearch.commons.alerting.util.nonOptionalTimeField -import org.opensearch.commons.alerting.util.optionalTimeField -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 - -private val logger = LogManager.getLogger(PPLMonitor::class.java) - -// TODO: probably 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. - * Only applicable if Monitor uses CRON schedule. Optional even if CRON schedule is used. - * @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: TimeValue? = null, - override val lastUpdateTime: Instant, - override val enabledTime: Instant?, - override val triggers: List, - override val schemaVersion: Int = 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 = MONITOR_V2_TYPE - - override fun fromDocument(id: String, version: Long): PPLMonitor = copy(id = id, version = version) - - init { - // SQL monitors are not yet supported - if (this.queryLanguage == QueryLanguage.SQL) { - throw IllegalStateException("Monitors with SQL queries are not supported") - } - - // for checking trigger ID uniqueness - val triggerIds = mutableSetOf() - triggers.forEach { trigger -> - require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique." } - } - - if (enabled) { - requireNotNull(enabledTime) - } else { - require(enabledTime == null) - } - - triggers.forEach { trigger -> - require(trigger is PPLTrigger) { "Incompatible trigger [${trigger.id}] for monitor type [$PPL_MONITOR_TYPE]" } - } - - // TODO: create setting for max triggers and check for max triggers here - } - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - id = sin.readString(), - version = sin.readLong(), - name = sin.readString(), - enabled = sin.readBoolean(), - schedule = Schedule.readFrom(sin), - lookBackWindow = TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME), - lastUpdateTime = sin.readInstant(), - enabledTime = sin.readOptionalInstant(), - triggers = sin.readList(PPLTrigger::readFrom), - schemaVersion = sin.readInt(), - queryLanguage = sin.readEnum(QueryLanguage::class.java), - query = sin.readString() - ) - - override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): 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(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?.toHumanReadableString(0)) - 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) - - 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) - } - - out.writeBoolean(lookBackWindow != null) - lookBackWindow?.let { out.writeString(lookBackWindow.toHumanReadableString(0)) } - - out.writeInstant(lastUpdateTime) - out.writeOptionalInstant(enabledTime) - out.writeVInt(triggers.size) - triggers.forEach { it.writeTo(out) } - out.writeInt(schemaVersion) - out.writeEnum(queryLanguage) - out.writeString(query) - } - - override fun asTemplateArg(): Map { - return mapOf( - _ID to id, - _VERSION to version, - NAME_FIELD to name, - ENABLED_FIELD to enabled, - SCHEDULE_FIELD to schedule, - LOOK_BACK_WINDOW_FIELD to lookBackWindow?.toHumanReadableString(0), - LAST_UPDATE_TIME_FIELD to lastUpdateTime.toEpochMilli(), - ENABLED_TIME_FIELD to enabledTime?.toEpochMilli(), - TRIGGERS_FIELD to triggers, - QUERY_LANGUAGE_FIELD to queryLanguage.value, - QUERY_FIELD to query - ) - } - - 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" - - // mock setting name used when parsing TimeValue - // TimeValue class is usually reserved for declaring settings, but we're using it - // outside that use case here, which is why we need these placeholders - private const val PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME = "ppl_monitor_look_back_window" - - @JvmStatic - @JvmOverloads - @Throws(IOException::class) - fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): PPLMonitor { - var name: String? = null - var monitorType: String = PPL_MONITOR_TYPE - var enabled = true - var schedule: Schedule? = null - var lookBackWindow: TimeValue? = null - var lastUpdateTime: Instant? = null - var enabledTime: Instant? = null - val triggers: MutableList = mutableListOf() - var schemaVersion = 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() - MONITOR_TYPE_FIELD -> monitorType = xcp.text() - ENABLED_FIELD -> enabled = xcp.booleanValue() - SCHEDULE_FIELD -> schedule = Schedule.parse(xcp) - LOOK_BACK_WINDOW_FIELD -> { - lookBackWindow = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { - val input = xcp.text() - TimeValue.parseTimeValue(input, PLACEHOLDER_LOOK_BACK_WINDOW_SETTING_NAME) // throws IllegalArgumentException if there's parsing error - } - } - LAST_UPDATE_TIME_FIELD -> lastUpdateTime = xcp.instant() - ENABLED_TIME_FIELD -> enabledTime = xcp.instant() - 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 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 \"$fieldName\" when parsing PPL Monitor") - } - } - - /* validations */ - - // ensure MonitorV2 XContent being parsed by PPLMonitor class is PPL Monitor type - if (monitorType != PPL_MONITOR_TYPE) { - throw IllegalArgumentException("Invalid monitor type: $monitorType") - } - - // ensure there's at least 1 trigger - if (triggers.isEmpty()) { - throw IllegalArgumentException("Monitor must include at least 1 trigger") - } - - // ensure the trigger suppress durations are valid - triggers.forEach { trigger -> - trigger.suppressDuration?.let { suppressDuration -> - // TODO: these max and min values are completely arbitrary, make them settings - val minValue = TimeValue.timeValueMinutes(1) - val maxValue = TimeValue.timeValueDays(5) - - require(suppressDuration <= maxValue) { "Suppress duration must be at most $maxValue but was $suppressDuration" } - - require(suppressDuration >= minValue) { "Suppress duration must be at least $minValue but was $suppressDuration" } - } - } - - // 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(queryLanguage) { "Query language is null" } - requireNotNull(query) { "Query is null" } - requireNotNull(lastUpdateTime) { "Last update time is null" } - - if (schedule is IntervalSchedule && lookBackWindow != null) { - throw IllegalArgumentException("Look back windows only supported for CRON schedules") - } - - if (queryLanguage == QueryLanguage.SQL) { - throw IllegalArgumentException("SQL queries are not supported. Please use a PPL query.") - } - - /* return PPLMonitor */ - return PPLMonitor( - id, - version, - name, - enabled, - schedule, - lookBackWindow, - lastUpdateTime, - enabledTime, - triggers, - schemaVersion, - queryLanguage, - query - ) - } - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt deleted file mode 100644 index 91c89eea..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLMonitorRunResult.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.ERROR_FIELD -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.MONITOR_V2_NAME_FIELD -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_END_FIELD -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.PERIOD_START_FIELD -import org.opensearch.commons.alerting.model.MonitorV2RunResult.Companion.TRIGGER_RESULTS_FIELD -import org.opensearch.commons.alerting.util.nonOptionalTimeField -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 -import java.time.Instant - -data class PPLMonitorRunResult( - override val monitorName: String, - override val error: Exception?, - override val periodStart: Instant, - override val periodEnd: Instant, - 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.readInstant(), // periodStart - sin.readInstant(), // periodEnd - 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.nonOptionalTimeField(PERIOD_START_FIELD, periodStart) - builder.nonOptionalTimeField(PERIOD_END_FIELD, periodEnd) - 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.writeInstant(periodStart) - out.writeInstant(periodEnd) - out.writeMap(triggerResults) - out.writeMap(pplQueryResults) - } - - companion object { - const val PPL_QUERY_RESULTS_FIELD = "ppl_query_results" - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt deleted file mode 100644 index eac6cc89..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTrigger.kt +++ /dev/null @@ -1,342 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.apache.logging.log4j.LogManager -import org.opensearch.common.CheckedFunction -import org.opensearch.common.UUIDs -import org.opensearch.common.unit.TimeValue -import org.opensearch.commons.alerting.model.TriggerV2.Companion.ACTIONS_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Companion.EXPIRE_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Companion.ID_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Companion.LAST_TRIGGERED_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Companion.NAME_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Companion.SEVERITY_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Companion.SUPPRESS_FIELD -import org.opensearch.commons.alerting.model.TriggerV2.Severity -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 - -private val logger = LogManager.getLogger(PPLTrigger::class.java) - -/** - * The PPL Trigger for PPL Monitors - * - * There are two types of PPLTrigger conditions: NUMBER_OF_RESULT and CUSTOM - * NUMBER_OF_RESULTS: triggers based on if the number of query results returned by the PPLMonitor - * query meets some threshold - * CUSTOM: triggers based on a custom condition that user specifies - * This trigger can operate in either result set or per-result mode and supports - * both numeric result conditions and custom conditions. - * - * 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 suppressDuration Optional duration for which alerts from this Trigger should be suppressed. - * Null indicates no suppression. - * @property expireDuration Duration 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, - * null otherwise. - * @property numResultsValue The threshold value for NUMBER_OF_RESULTS conditions. Required if using NUMBER_OF_RESULTS conditions, - * null otherwise. - * @property customCondition A custom condition expression. Required if using CUSTOM conditions, - * null otherwise. - */ -data class PPLTrigger( - override val id: String = UUIDs.base64UUID(), - override val name: String, - override val severity: Severity, - override val suppressDuration: TimeValue?, - override val expireDuration: TimeValue, - override var lastTriggeredTime: Instant?, - override val actions: List, - val mode: TriggerMode, // result_set or per_result - val conditionType: ConditionType, - val numResultsCondition: NumResultsCondition?, - val numResultsValue: Long?, - val customCondition: String? -) : TriggerV2 { - - @Throws(IOException::class) - constructor(sin: StreamInput) : this( - sin.readString(), // id - sin.readString(), // name - sin.readEnum(Severity::class.java), // severity - // parseTimeValue() is typically used to parse OpenSearch settings - // the second param is supposed to accept a setting name, but here we're passing in our own name - TimeValue.parseTimeValue(sin.readOptionalString(), PLACEHOLDER_SUPPRESS_SETTING_NAME), // suppressDuration - TimeValue.parseTimeValue(sin.readString(), PLACEHOLDER_EXPIRE_SETTING_NAME), // 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.writeBoolean(suppressDuration != null) - suppressDuration?.let { out.writeString(suppressDuration.toHumanReadableString(0)) } - - out.writeString(expireDuration.toHumanReadableString(0)) - 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) - builder.field(SUPPRESS_FIELD, suppressDuration?.toHumanReadableString(0)) - builder.field(EXPIRE_FIELD, expireDuration.toHumanReadableString(0)) - 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, - SUPPRESS_FIELD to suppressDuration?.toHumanReadableString(0), - EXPIRE_FIELD to expireDuration?.toHumanReadableString(0), - 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" - - // mock setting name used when parsing TimeValue - // TimeValue class is usually reserved for declaring settings, but we're using it - // outside that use case here, which is why we need these placeholders - private const val PLACEHOLDER_SUPPRESS_SETTING_NAME = "ppl_trigger_suppress_duration" - private const val PLACEHOLDER_EXPIRE_SETTING_NAME = "ppl_trigger_expire_duration" - - 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 suppressDuration: TimeValue? = null - var expireDuration: TimeValue = TimeValue.timeValueDays(7) // default to 7 days // TODO: add this as a setting - 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(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) // outer trigger object start - - 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 -> { - numResultsCondition = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { - 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 }}") - enumMatchResult - } - } - NUM_RESULTS_VALUE_FIELD -> { - numResultsValue = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { - xcp.longValue() - } - } - CUSTOM_CONDITION_FIELD -> { - customCondition = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { - xcp.text() - } - } - SUPPRESS_FIELD -> { - suppressDuration = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) { - null - } else { - val input = xcp.text() - TimeValue.parseTimeValue(input, PLACEHOLDER_SUPPRESS_SETTING_NAME) // throws IllegalArgumentException if there's parsing error - } - } - EXPIRE_FIELD -> { - if (xcp.currentToken() != XContentParser.Token.VALUE_NULL) { // if expire field is null, skip reading it and let it retain the default value - val input = xcp.text() - expireDuration = TimeValue.parseTimeValue(input, PLACEHOLDER_EXPIRE_SETTING_NAME) // throws IllegalArgumentException if there's parsing error - } - } - 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 $fieldName when parsing PPL Trigger") - } - } - - /* 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" } - - when (conditionType) { - ConditionType.NUMBER_OF_RESULTS -> { - requireNotNull(numResultsCondition) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_CONDITION_FIELD must be included" } - requireNotNull(numResultsValue) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $NUM_RESULTS_VALUE_FIELD must be included" } - require(customCondition == null) { "if trigger condition is of type ${ConditionType.NUMBER_OF_RESULTS.value}, $CUSTOM_CONDITION_FIELD must not be included" } - } - ConditionType.CUSTOM -> { - requireNotNull(customCondition) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $CUSTOM_CONDITION_FIELD must be included" } - require(numResultsCondition == null) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $NUM_RESULTS_CONDITION_FIELD must not be included" } - require(numResultsValue == null) { "if trigger condition is of type ${ConditionType.CUSTOM.value}, $NUM_RESULTS_VALUE_FIELD must not be included" } - } - } - - // 3. prepare and return PPLTrigger object - return PPLTrigger( - id, - name, - severity, - suppressDuration, - expireDuration, - lastTriggeredTime, - actions, - mode, - conditionType, - numResultsCondition, - numResultsValue, - customCondition - ) - } - - @JvmStatic - @Throws(IOException::class) - fun readFrom(sin: StreamInput): PPLTrigger { - return PPLTrigger(sin) - } - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt deleted file mode 100644 index 1723d04f..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/PPLTriggerRunResult.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.opensearch.commons.alerting.alerts.AlertError -import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.ERROR_FIELD -import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.NAME_FIELD -import org.opensearch.commons.alerting.model.TriggerV2RunResult.Companion.TRIGGERED_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 -import java.time.Instant - -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/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt deleted file mode 100644 index a6cb9e27..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.opensearch.commons.alerting.model - -import org.opensearch.common.unit.TimeValue -import org.opensearch.commons.alerting.model.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 suppressDuration: TimeValue? - val expireDuration: TimeValue? - 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 { - const val ID_FIELD = "id" - const val NAME_FIELD = "name" - const val SEVERITY_FIELD = "severity" - const val SUPPRESS_FIELD = "suppress" - const val LAST_TRIGGERED_FIELD = "last_triggered_time" - const val EXPIRE_FIELD = "expires" - const val ACTIONS_FIELD = "actions" - } -} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt deleted file mode 100644 index 3e1796b4..00000000 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/TriggerV2RunResult.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.opensearch.commons.alerting.model - -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/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt b/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt deleted file mode 100644 index f03ec056..00000000 --- a/src/main/kotlin/org/opensearch/commons/ppl/PPLPluginInterface.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.opensearch.commons.ppl - -import org.opensearch.commons.utils.recreateObject -import org.opensearch.core.action.ActionListener -import org.opensearch.core.action.ActionResponse -import org.opensearch.core.common.io.stream.Writeable -import org.opensearch.sql.plugin.transport.PPLQueryAction -import org.opensearch.sql.plugin.transport.TransportPPLQueryRequest -import org.opensearch.sql.plugin.transport.TransportPPLQueryResponse -import org.opensearch.transport.client.node.NodeClient - -/** - * Various transport action plugin interfaces for the SQL/PPL plugin - */ -object PPLPluginInterface { - fun executeQuery( - client: NodeClient, - request: TransportPPLQueryRequest, - listener: ActionListener - ) { - client.execute( - PPLQueryAction.INSTANCE, - request, - wrapActionListener(listener) { response -> recreateObject(response) { TransportPPLQueryResponse(it) } } - ) - } - - /** - * Wrap action listener on concrete response class by a new created one on ActionResponse. - * This is required because the response may be loaded by different classloader across plugins. - * The onResponse(ActionResponse) avoids type cast exception and give a chance to recreate - * the response object. - */ - @Suppress("UNCHECKED_CAST") - private fun wrapActionListener( - listener: ActionListener, - recreate: (Writeable) -> Response - ): ActionListener { - return object : ActionListener { - override fun onResponse(response: ActionResponse) { - val recreated = recreate(response) - listener.onResponse(recreated) - } - - override fun onFailure(exception: java.lang.Exception) { - listener.onFailure(exception) - } - } as ActionListener - } -} From b847e4c34f031ecc7a089761911cd7be31e6d127 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 10 Sep 2025 14:32:41 -0700 Subject: [PATCH 30/30] removing sql dependencies from common utils to move to alerting --- build.gradle | 66 +++++----------------------------------------------- 1 file changed, 6 insertions(+), 60 deletions(-) diff --git a/build.gradle b/build.gradle index 727d92ed..a08f9368 100644 --- a/build.gradle +++ b/build.gradle @@ -10,14 +10,6 @@ buildscript { isSnapshot = "true" == System.getProperty("build.snapshot", "true") buildVersionQualifier = System.getProperty("build.version_qualifier", "") kotlin_version = System.getProperty("kotlin.version", "1.9.25") - version_tokens = opensearch_version.tokenize('-') - opensearch_build = version_tokens[0] + '.0' - if (buildVersionQualifier) { - opensearch_build += "-${buildVersionQualifier}" - } - if (isSnapshot) { - opensearch_build += "-SNAPSHOT" - } } repositories { @@ -39,7 +31,6 @@ plugins { id 'java-library' id 'maven-publish' id 'com.diffplug.spotless' version '6.25.0' - id "io.freefair.lombok" version "8.14" } repositories { @@ -76,25 +67,12 @@ apply from: 'build-tools/opensearchplugin-coverage.gradle' apply plugin: 'opensearch.java-agent' configurations { - zipArchive ktlint { - resolutionStrategy { - force "ch.qos.logback:logback-classic:1.5.16" - force "ch.qos.logback:logback-core:1.5.16" - } - } -} - -def sqlJarDirectory = "$buildDir/dependencies/opensearch-sql-plugin" - -task addJarsToClasspath(type: Copy) { - from(fileTree(dir: sqlJarDirectory)) { - include "opensearch-sql-${opensearch_build}.jar" - include "ppl-${opensearch_build}.jar" - include "protocol-${opensearch_build}.jar" - include "core-${opensearch_build}.jar" + resolutionStrategy { + force "ch.qos.logback:logback-classic:1.5.16" + force "ch.qos.logback:logback-core:1.5.16" + } } - into("$buildDir/classes") } dependencies { @@ -112,38 +90,11 @@ dependencies { testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.cronutils:cron-utils:9.1.6" testImplementation "commons-validator:commons-validator:1.7" - implementation 'org.json:json:20240303' - - implementation fileTree(dir: sqlJarDirectory, include: ["opensearch-sql-thin-${opensearch_build}.jar", "ppl-${opensearch_build}.jar", "protocol-${opensearch_build}.jar", "core-${opensearch_build}.jar"]) - - zipArchive group: 'org.opensearch.plugin', name:'opensearch-sql-plugin', version: "${opensearch_build}" + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' ktlint "com.pinterest:ktlint:0.47.1" } -task extractSqlJar(type: Copy) { - mustRunAfter() - from(zipTree(configurations.zipArchive.find { it.name.startsWith("opensearch-sql-plugin") })) - into sqlJarDirectory -} - -task extractSqlClass(type: Copy, dependsOn: [extractSqlJar]) { - from zipTree("${sqlJarDirectory}/opensearch-sql-${opensearch_build}.jar") - into("$buildDir/opensearch-sql") - include 'org/opensearch/sql/**' -} - -task replaceSqlJar(type: Jar, dependsOn: [extractSqlClass]) { - from("$buildDir/opensearch-sql") - archiveFileName = "opensearch-sql-thin-${opensearch_build}.jar" - destinationDirectory = file(sqlJarDirectory) - doLast { - file("${sqlJarDirectory}/opensearch-sql-${opensearch_build}.jar").delete() - } -} - -tasks.addJarsToClasspath.dependsOn(replaceSqlJar) - test { useJUnitPlatform() testLogging { @@ -190,12 +141,7 @@ tasks.register('ktlintFormat', JavaExec) { args "-F", "src/**/*.kt" } -compileJava { - dependsOn addJarsToClasspath -} - compileKotlin { - dependsOn addJarsToClasspath kotlinOptions { freeCompilerArgs = ['-Xjsr305=strict'] jvmTarget = "21" @@ -286,4 +232,4 @@ task updateVersion { // Include the required files that needs to be updated with new Version ant.replaceregexp(file:'build.gradle', match: '"opensearch.version", "\\d.*"', replace: '"opensearch.version", "' + newVersion.tokenize('-')[0] + '-SNAPSHOT"', flags:'g', byline:true) } -} +} \ No newline at end of file