Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions alerting/src/main/kotlin/org/opensearch/alerting/modelv2/AlertV2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.alerting.modelv2

import org.opensearch.alerting.core.util.nonOptionalTimeField
import org.opensearch.alerting.modelv2.TriggerV2.Severity
import org.opensearch.common.lucene.uid.Versions
import org.opensearch.commons.alerting.util.instant
import org.opensearch.commons.alerting.util.optionalUserField
import org.opensearch.commons.authuser.User
import org.opensearch.core.common.io.stream.StreamInput
import org.opensearch.core.common.io.stream.StreamOutput
import org.opensearch.core.common.io.stream.Writeable
import org.opensearch.core.xcontent.ToXContent
import org.opensearch.core.xcontent.XContentBuilder
import org.opensearch.core.xcontent.XContentParser
import org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken
import java.io.IOException
import java.time.Instant

/**
* Alert generated by Alerting V2
* An alert is created when a Trigger's trigger conditions are met.
*
* @property id Alert ID. Defaults to [NO_ID].
* @property version Version number of the Alert. Defaults to [NO_VERSION].
* @property schemaVersion Version of the alerting-alerts index schema when this Alert was indexed. Defaults to [NO_SCHEMA_VERSION].
* @property monitorId ID of the Monitor that generated this Alert.
* @property monitorName Name of the Monitor that generated this Alert.
* @property monitorVersion Version of the Monitor at the time it generated this Alert.
* @property triggerId ID of the Trigger in the Monitor that generated this alert.
* @property triggerName Name of the trigger in the Monitor that generated this alert.
* @property queryResults Results from the Monitor's query that caused the Trigger to fire.
* @property triggeredTime Timestamp for when the Alert was generated.
* @property errorMessage Optional error message if there were issues during Trigger execution.
* Null indicates no errors occurred.
* @property severity Severity level of the alert (e.g., "HIGH", "MEDIUM", "LOW").
* @property executionId Optional ID for the Monitor execution that generated this Alert.
*
* @see MonitorV2 For the monitor that generates alerts
* @see TriggerV2 For the trigger conditions that create alerts
*
* Lifecycle:
* 1. AlertV2 is generated when a TriggerV2's condition is met. The TriggerV2 fires and forgets the AlertV2.
* 2. AlertV2 is stored in the alerts index. AlertV2s are stateless. (e.g. they are never ACTIVE or COMPLETED)
* 3. AlertV2 is soft deleted after its expire duration (determined by its trigger), and archived in an alert history index
Comment thread
AWSHurneyt marked this conversation as resolved.
* 4. Based on the alert v2 history retention period, the AlertV2 is permanently deleted
*/
data class AlertV2(
val id: String = NO_ID,
val version: Long = NO_VERSION,
val schemaVersion: Int = NO_SCHEMA_VERSION,
val monitorId: String,
val monitorName: String,
val monitorVersion: Long,
val monitorUser: User?,
val triggerId: String,
val triggerName: String,
val query: String,
val queryResults: Map<String, Any>,
val triggeredTime: Instant,
val errorMessage: String? = null,
val severity: Severity,
val executionId: String? = null
) : Writeable, ToXContent {
@Throws(IOException::class)
constructor(sin: StreamInput) : this(
id = sin.readString(),
version = sin.readLong(),
schemaVersion = sin.readInt(),
monitorId = sin.readString(),
monitorName = sin.readString(),
monitorVersion = sin.readLong(),
monitorUser = if (sin.readBoolean()) {
User(sin)
} else {
null
},
triggerId = sin.readString(),
triggerName = sin.readString(),
query = sin.readString(),
queryResults = sin.readMap(),
triggeredTime = sin.readInstant(),
errorMessage = sin.readOptionalString(),
severity = sin.readEnum(Severity::class.java),
executionId = sin.readOptionalString()
)

@Throws(IOException::class)
override fun writeTo(out: StreamOutput) {
out.writeString(id)
out.writeLong(version)
out.writeInt(schemaVersion)
out.writeString(monitorId)
out.writeString(monitorName)
out.writeLong(monitorVersion)
out.writeBoolean(monitorUser != null)
monitorUser?.writeTo(out)
out.writeString(triggerId)
out.writeString(triggerName)
out.writeString(query)
out.writeMap(queryResults)
out.writeInstant(triggeredTime)
out.writeOptionalString(errorMessage)
out.writeEnum(severity)
out.writeOptionalString(executionId)
}

override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder {
return createXContentBuilder(builder, false)
}

fun toXContentWithUser(builder: XContentBuilder): XContentBuilder {
return createXContentBuilder(builder, true)
}

private fun createXContentBuilder(builder: XContentBuilder, withUser: Boolean): XContentBuilder {
builder.startObject()
.field(ALERT_V2_ID_FIELD, id)
.field(ALERT_V2_VERSION_FIELD, version)
.field(MONITOR_V2_ID_FIELD, monitorId)
.field(SCHEMA_VERSION_FIELD, schemaVersion)
.field(MONITOR_V2_VERSION_FIELD, monitorVersion)
.field(MONITOR_V2_NAME_FIELD, monitorName)
.field(EXECUTION_ID_FIELD, executionId)
.field(TRIGGER_V2_ID_FIELD, triggerId)
.field(TRIGGER_V2_NAME_FIELD, triggerName)
.field(QUERY_FIELD, query)
.field(QUERY_RESULTS_FIELD, queryResults)
.field(ERROR_MESSAGE_FIELD, errorMessage)
.field(SEVERITY_FIELD, severity.value)
.nonOptionalTimeField(TRIGGERED_TIME_FIELD, triggeredTime)

if (withUser) {
builder.optionalUserField(MONITOR_V2_USER_FIELD, monitorUser)
}

builder.endObject()

return builder
}

fun asTemplateArg(): Map<String, Any?> {
return mapOf(
ALERT_V2_ID_FIELD to id,
ALERT_V2_VERSION_FIELD to version,
ERROR_MESSAGE_FIELD to errorMessage,
EXECUTION_ID_FIELD to executionId,
SEVERITY_FIELD to severity.value
)
}

companion object {
const val ALERT_V2_ID_FIELD = "id"
const val ALERT_V2_VERSION_FIELD = "version"
const val MONITOR_V2_ID_FIELD = "monitor_v2_id"
const val MONITOR_V2_VERSION_FIELD = "monitor_v2_version"
const val MONITOR_V2_NAME_FIELD = "monitor_v2_name"
const val MONITOR_V2_USER_FIELD = "monitor_v2_user"
const val TRIGGER_V2_ID_FIELD = "trigger_v2_id"
const val TRIGGER_V2_NAME_FIELD = "trigger_v2_name"
const val TRIGGERED_TIME_FIELD = "triggered_time"
const val QUERY_FIELD = "query"
const val QUERY_RESULTS_FIELD = "query_results"
const val ERROR_MESSAGE_FIELD = "error_message"
const val EXECUTION_ID_FIELD = "execution_id"
const val SEVERITY_FIELD = "severity"
const val SCHEMA_VERSION_FIELD = "schema_version"

const val NO_ID = ""
const val NO_VERSION = Versions.NOT_FOUND
const val NO_SCHEMA_VERSION = 0

@JvmStatic
@JvmOverloads
@Throws(IOException::class)
fun parse(xcp: XContentParser, id: String = NO_ID, version: Long = NO_VERSION): AlertV2 {
Comment thread
eirsep marked this conversation as resolved.
var schemaVersion = NO_SCHEMA_VERSION
lateinit var monitorId: String
lateinit var monitorName: String
var monitorVersion: Long = Versions.NOT_FOUND
var monitorUser: User? = null
lateinit var triggerId: String
lateinit var triggerName: String
lateinit var query: String
var queryResults: Map<String, Any> = mapOf()
lateinit var severity: Severity
var triggeredTime: Instant? = null
var errorMessage: String? = null
var executionId: String? = null

ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
while (xcp.nextToken() != XContentParser.Token.END_OBJECT) {
val fieldName = xcp.currentName()
xcp.nextToken()

when (fieldName) {
MONITOR_V2_ID_FIELD -> monitorId = xcp.text()
SCHEMA_VERSION_FIELD -> schemaVersion = xcp.intValue()
MONITOR_V2_NAME_FIELD -> monitorName = xcp.text()
MONITOR_V2_VERSION_FIELD -> monitorVersion = xcp.longValue()
MONITOR_V2_USER_FIELD ->
monitorUser = if (xcp.currentToken() == XContentParser.Token.VALUE_NULL) {
null
} else {
User.parse(xcp)
}
TRIGGER_V2_ID_FIELD -> triggerId = xcp.text()
TRIGGER_V2_NAME_FIELD -> triggerName = xcp.text()
QUERY_FIELD -> query = xcp.text()
QUERY_RESULTS_FIELD -> queryResults = xcp.map()
TRIGGERED_TIME_FIELD -> triggeredTime = xcp.instant()
ERROR_MESSAGE_FIELD -> errorMessage = xcp.textOrNull()
EXECUTION_ID_FIELD -> executionId = xcp.textOrNull()
TriggerV2.SEVERITY_FIELD -> {
val input = xcp.text()
val enumMatchResult = Severity.enumFromString(input)
?: throw IllegalArgumentException(
"Invalid value for ${TriggerV2.SEVERITY_FIELD}: $input. " +
"Supported values are ${Severity.entries.map { it.value }}"
)
severity = enumMatchResult
}
}
}

return AlertV2(
id = id,
version = version,
schemaVersion = schemaVersion,
monitorId = requireNotNull(monitorId),
monitorName = requireNotNull(monitorName),
monitorVersion = monitorVersion,
monitorUser = monitorUser,
triggerId = requireNotNull(triggerId),
triggerName = requireNotNull(triggerName),
query = requireNotNull(query),
queryResults = requireNotNull(queryResults),
triggeredTime = requireNotNull(triggeredTime),
errorMessage = errorMessage,
severity = severity,
executionId = executionId
)
}

@JvmStatic
@Throws(IOException::class)
fun readFrom(sin: StreamInput): AlertV2 {
return AlertV2(sin)
}
}
}
Loading
Loading