diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/Alert.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/Alert.kt index c251e540..90c94e3c 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/Alert.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/Alert.kt @@ -125,6 +125,24 @@ data class Alert( aggregationResultBucket = null, findingIds = findingIds, relatedDocIds = relatedDocIds ) + constructor( + id: String = NO_ID, + monitor: Monitor, + trigger: NoOpTrigger, + startTime: Instant, + lastNotificationTime: Instant?, + state: State = State.ERROR, + errorMessage: String, + errorHistory: List = mutableListOf(), + schemaVersion: Int = NO_SCHEMA_VERSION + ) : this( + id = id, monitorId = monitor.id, monitorName = monitor.name, monitorVersion = monitor.version, monitorUser = monitor.user, + triggerId = trigger.id, triggerName = trigger.name, state = state, startTime = startTime, + lastNotificationTime = lastNotificationTime, errorMessage = errorMessage, errorHistory = errorHistory, + severity = trigger.severity, actionExecutionResults = listOf(), schemaVersion = schemaVersion, + aggregationResultBucket = null, findingIds = listOf(), relatedDocIds = listOf() + ) + enum class State { ACTIVE, ACKNOWLEDGED, COMPLETED, ERROR, DELETED } diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/Monitor.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/Monitor.kt index e32f1b67..a7e7b639 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/Monitor.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/Monitor.kt @@ -50,6 +50,9 @@ data class Monitor( // Ensure that trigger ids are unique within a monitor val triggerIds = mutableSetOf() triggers.forEach { trigger -> + // NoOpTrigger is only used in "Monitor Error Alerts" as a placeholder + require(trigger !is NoOpTrigger) + require(triggerIds.add(trigger.id)) { "Duplicate trigger id: ${trigger.id}. Trigger ids must be unique." } // Verify Trigger type based on Monitor type when (monitorType) { diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/NoOpTrigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/NoOpTrigger.kt new file mode 100644 index 00000000..92456492 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/NoOpTrigger.kt @@ -0,0 +1,76 @@ +package org.opensearch.commons.alerting.model + +import org.opensearch.common.CheckedFunction +import org.opensearch.common.UUIDs +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.alerting.model.action.Action +import org.opensearch.core.ParseField +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 java.io.IOException + +data class NoOpTrigger( + override val id: String = UUIDs.base64UUID(), + override val name: String = "NoOp trigger", + override val severity: String = "", + override val actions: List = listOf(), +) : Trigger { + + @Throws(IOException::class) + constructor(sin: StreamInput) : this() + + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + builder.startObject() + .startObject(NOOP_TRIGGER_FIELD) + .field(ID_FIELD, id) + .endObject() + .endObject() + return builder + } + + override fun name(): String { + return NOOP_TRIGGER_FIELD + } + + fun asTemplateArg(): Map { + return mapOf() + } + + @Throws(IOException::class) + override fun writeTo(out: StreamOutput) { + } + + companion object { + const val ID_FIELD = "id" + const val NOOP_TRIGGER_FIELD = "noop_trigger" + val XCONTENT_REGISTRY = NamedXContentRegistry.Entry( + Trigger::class.java, ParseField(NOOP_TRIGGER_FIELD), + CheckedFunction { parseInner(it) } + ) + + @JvmStatic @Throws(IOException::class) + fun parseInner(xcp: XContentParser): NoOpTrigger { + var id = UUIDs.base64UUID() + if (xcp.currentToken() == XContentParser.Token.START_OBJECT) xcp.nextToken() + if (xcp.currentName() == ID_FIELD) { + xcp.nextToken() + id = xcp.text() + xcp.nextToken() + } + if (xcp.currentToken() != XContentParser.Token.END_OBJECT) { + XContentParserUtils.throwUnknownToken(xcp.currentToken(), xcp.tokenLocation) + } + return NoOpTrigger(id = id) + } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): NoOpTrigger { + return NoOpTrigger(sin) + } + } +} diff --git a/src/main/kotlin/org/opensearch/commons/alerting/model/Trigger.kt b/src/main/kotlin/org/opensearch/commons/alerting/model/Trigger.kt index 4b83fed3..254b6401 100644 --- a/src/main/kotlin/org/opensearch/commons/alerting/model/Trigger.kt +++ b/src/main/kotlin/org/opensearch/commons/alerting/model/Trigger.kt @@ -12,7 +12,8 @@ interface Trigger : BaseModel { enum class Type(val value: String) { DOCUMENT_LEVEL_TRIGGER(DocumentLevelTrigger.DOCUMENT_LEVEL_TRIGGER_FIELD), QUERY_LEVEL_TRIGGER(QueryLevelTrigger.QUERY_LEVEL_TRIGGER_FIELD), - BUCKET_LEVEL_TRIGGER(BucketLevelTrigger.BUCKET_LEVEL_TRIGGER_FIELD); + BUCKET_LEVEL_TRIGGER(BucketLevelTrigger.BUCKET_LEVEL_TRIGGER_FIELD), + NOOP_TRIGGER(NoOpTrigger.NOOP_TRIGGER_FIELD); override fun toString(): String { return value diff --git a/src/test/kotlin/org/opensearch/commons/alerting/TestHelpers.kt b/src/test/kotlin/org/opensearch/commons/alerting/TestHelpers.kt index a16fee63..685898ec 100644 --- a/src/test/kotlin/org/opensearch/commons/alerting/TestHelpers.kt +++ b/src/test/kotlin/org/opensearch/commons/alerting/TestHelpers.kt @@ -29,6 +29,7 @@ import org.opensearch.commons.alerting.model.Finding import org.opensearch.commons.alerting.model.Input import org.opensearch.commons.alerting.model.IntervalSchedule import org.opensearch.commons.alerting.model.Monitor +import org.opensearch.commons.alerting.model.NoOpTrigger import org.opensearch.commons.alerting.model.QueryLevelTrigger import org.opensearch.commons.alerting.model.Schedule import org.opensearch.commons.alerting.model.SearchInput @@ -395,7 +396,8 @@ fun xContentRegistry(): NamedXContentRegistry { DocLevelMonitorInput.XCONTENT_REGISTRY, QueryLevelTrigger.XCONTENT_REGISTRY, BucketLevelTrigger.XCONTENT_REGISTRY, - DocumentLevelTrigger.XCONTENT_REGISTRY + DocumentLevelTrigger.XCONTENT_REGISTRY, + NoOpTrigger.XCONTENT_REGISTRY ) + SearchModule(Settings.EMPTY, emptyList()).namedXContents ) } diff --git a/src/test/kotlin/org/opensearch/commons/alerting/model/XContentTests.kt b/src/test/kotlin/org/opensearch/commons/alerting/model/XContentTests.kt index fa93da5f..f8c98842 100644 --- a/src/test/kotlin/org/opensearch/commons/alerting/model/XContentTests.kt +++ b/src/test/kotlin/org/opensearch/commons/alerting/model/XContentTests.kt @@ -30,6 +30,7 @@ import org.opensearch.core.xcontent.ToXContent import org.opensearch.index.query.QueryBuilders import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.test.OpenSearchTestCase +import java.time.Instant import java.time.temporal.ChronoUnit import kotlin.test.assertFailsWith @@ -179,6 +180,16 @@ class XContentTests { Assertions.assertEquals(trigger, parsedTrigger, "Round tripping BucketLevelTrigger doesn't work") } + @Test + fun `test no-op trigger parsing`() { + val trigger = NoOpTrigger() + + val triggerString = trigger.toXContent(builder(), ToXContent.EMPTY_PARAMS).string() + val parsedTrigger = Trigger.parse(parser(triggerString)) + + Assertions.assertEquals(trigger, parsedTrigger, "Round tripping NoOpTrigger doesn't work") + } + @Test fun `test creating a monitor with duplicate trigger ids fails`() { try { @@ -357,6 +368,16 @@ class XContentTests { assertEquals("Round tripping alert doesn't work", alert, parsedAlert) } + @Test + fun `test alert parsing with noop trigger`() { + val monitor = randomQueryLevelMonitor() + val alert = Alert( + monitor = monitor, trigger = NoOpTrigger(), startTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorMessage = "some error", lastNotificationTime = Instant.now() + ) + assertEquals("Round tripping alert doesn't work", alert.triggerName, "NoOp trigger") + } + @Test fun `test alert parsing without user`() { val alertStr = "{\"id\":\"\",\"version\":-1,\"monitor_id\":\"\",\"schema_version\":0,\"monitor_version\":1," +