From 4cebaa3262cf32f913952972875b440530d3bc5d Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sun, 26 May 2024 17:10:41 -0700 Subject: [PATCH 01/22] initial commit, functional but needs refactoring Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 49 +- .../org/opensearch/alerting/AlertingPlugin.kt | 24 +- .../alerting/BucketLevelMonitorRunner.kt | 27 +- .../alerting/QueryLevelMonitorRunner.kt | 18 +- .../alerting/alerts/AlertIndices.kt | 95 +++- .../opensearch/alerting/model/AlertContext.kt | 16 +- .../opensearch/alerting/notes/NotesIndices.kt | 431 ++++++++++++++++++ .../resthandler/RestDeleteNoteAction.kt | 53 +++ .../resthandler/RestIndexNoteAction.kt | 108 +++++ .../resthandler/RestSearchMonitorAction.kt | 4 + .../resthandler/RestSearchNoteAction.kt | 118 +++++ .../BucketLevelTriggerExecutionContext.kt | 9 +- .../QueryLevelTriggerExecutionContext.kt | 9 +- .../alerting/settings/AlertingSettings.kt | 38 ++ .../transport/TransportDeleteNoteAction.kt | 167 +++++++ .../TransportDocLevelMonitorFanOutAction.kt | 3 + .../transport/TransportIndexNoteAction.kt | 415 +++++++++++++++++ .../transport/TransportSearchNoteAction.kt | 167 +++++++ .../opensearch/alerting/util/IndexUtils.kt | 12 + .../opensearch/alerting/util/NotesUtils.kt | 88 ++++ .../alerting/notes/alerting_notes.json | 52 +++ 21 files changed, 1874 insertions(+), 29 deletions(-) create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt create mode 100644 alerting/src/main/resources/org/opensearch/alerting/notes/alerting_notes.json diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index 6b820cf36..3678f83b3 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -30,6 +30,7 @@ import org.opensearch.alerting.script.DocumentLevelTriggerExecutionContext import org.opensearch.alerting.script.QueryLevelTriggerExecutionContext import org.opensearch.alerting.util.IndexUtils import org.opensearch.alerting.util.MAX_SEARCH_SIZE +import org.opensearch.alerting.util.NotesUtils import org.opensearch.alerting.util.getBucketKeysHash import org.opensearch.alerting.workflow.WorkflowRunContext import org.opensearch.client.Client @@ -46,6 +47,7 @@ import org.opensearch.commons.alerting.model.BucketLevelTrigger import org.opensearch.commons.alerting.model.DataSources import org.opensearch.commons.alerting.model.Monitor import org.opensearch.commons.alerting.model.NoOpTrigger +import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.Trigger import org.opensearch.commons.alerting.model.Workflow import org.opensearch.commons.alerting.model.action.AlertCategory @@ -474,6 +476,21 @@ class AlertService( } ?: listOf() } + /** + * Performs a Search request to retrieve the Notes associated with the + * Alert with the given ID. + * + * Searches for the n most recently created Notes based on maxNotes + */ + suspend fun getNotesForAlertNotification(alertId: String, maxNotes: Int): List { + val allNotes = NotesUtils.getNotesByAlertIDs(client, listOf(alertId)) + val sortedNotes = allNotes.sortedByDescending { it.time } + if (sortedNotes.size <= maxNotes) { + return sortedNotes + } + return sortedNotes.slice(0 until maxNotes) + } + suspend fun upsertMonitorErrorAlert( monitor: Monitor, errorMessage: String, @@ -684,6 +701,8 @@ class AlertService( val alertsIndex = dataSources.alertsIndex val alertsHistoryIndex = dataSources.alertsHistoryIndex + val notesToDeleteIDs = mutableListOf() + var requestsToRetry = alerts.flatMap { alert -> // We don't want to set the version when saving alerts because the MonitorRunner has first priority when writing alerts. // In the rare event that a user acknowledges an alert between when it's read and when it's written @@ -727,16 +746,39 @@ class AlertService( throw IllegalStateException("Unexpected attempt to save ${alert.state} alert: $alert") } Alert.State.COMPLETED -> { +// val requestsList = mutableListOf>( +// DeleteRequest(alertsIndex, alert.id) +// .routing(routingId) +// ) +// // Only add completed alert to respective history indices if history is enabled +// if (alertIndices.isAlertHistoryEnabled()) { +// requestsList.add( +// IndexRequest(alertsHistoryIndex) +// .routing(routingId) +// .source(alert.toXContentWithUser(XContentFactory.jsonBuilder())) +// .id(alert.id) +// ) +// } else { +// // Prepare Alert's Notes for deletion as well +// val notes = NotesUtils.searchNotesByAlertID(client, listOf(alert.id)) +// notes.forEach { notesToDeleteIDs.add(it.id) } +// } +// Collections.unmodifiableList(requestsList) listOfNotNull>( DeleteRequest(alertsIndex, alert.id) .routing(routingId), - // Only add completed alert to history index if history is enabled if (alertIndices.isAlertHistoryEnabled()) { + // Only add completed alert to history index if history is enabled IndexRequest(alertsHistoryIndex) .routing(routingId) .source(alert.toXContentWithUser(XContentFactory.jsonBuilder())) .id(alert.id) - } else null + } else { + // Otherwise, prepare the Alert's Notes for deletion, and don't include + // a request to index the Alert to an Alert history index + notesToDeleteIDs.addAll(NotesUtils.getNoteIDsByAlertIDs(client, listOf(alert.id))) + null + } ) } } @@ -756,6 +798,9 @@ class AlertService( throw ExceptionsHelper.convertToOpenSearchException(retryCause) } } + + // delete all the Notes of any Alerts that were deleted + NotesUtils.deleteNotes(client, notesToDeleteIDs) } /** diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index d531d844d..9e6e7b7b8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -25,9 +25,11 @@ import org.opensearch.alerting.core.resthandler.RestScheduledJobStatsHandler import org.opensearch.alerting.core.schedule.JobScheduler import org.opensearch.alerting.core.settings.LegacyOpenDistroScheduledJobSettings import org.opensearch.alerting.core.settings.ScheduledJobSettings +import org.opensearch.alerting.notes.NotesIndices import org.opensearch.alerting.resthandler.RestAcknowledgeAlertAction import org.opensearch.alerting.resthandler.RestAcknowledgeChainedAlertAction import org.opensearch.alerting.resthandler.RestDeleteMonitorAction +import org.opensearch.alerting.resthandler.RestDeleteNoteAction import org.opensearch.alerting.resthandler.RestDeleteWorkflowAction import org.opensearch.alerting.resthandler.RestExecuteMonitorAction import org.opensearch.alerting.resthandler.RestExecuteWorkflowAction @@ -41,10 +43,12 @@ import org.opensearch.alerting.resthandler.RestGetRemoteIndexesAction import org.opensearch.alerting.resthandler.RestGetWorkflowAction import org.opensearch.alerting.resthandler.RestGetWorkflowAlertsAction import org.opensearch.alerting.resthandler.RestIndexMonitorAction +import org.opensearch.alerting.resthandler.RestIndexNoteAction import org.opensearch.alerting.resthandler.RestIndexWorkflowAction import org.opensearch.alerting.resthandler.RestSearchEmailAccountAction import org.opensearch.alerting.resthandler.RestSearchEmailGroupAction import org.opensearch.alerting.resthandler.RestSearchMonitorAction +import org.opensearch.alerting.resthandler.RestSearchNoteAction import org.opensearch.alerting.script.TriggerScript import org.opensearch.alerting.service.DeleteMonitorService import org.opensearch.alerting.settings.AlertingSettings @@ -55,6 +59,7 @@ import org.opensearch.alerting.settings.LegacyOpenDistroDestinationSettings import org.opensearch.alerting.transport.TransportAcknowledgeAlertAction import org.opensearch.alerting.transport.TransportAcknowledgeChainedAlertAction import org.opensearch.alerting.transport.TransportDeleteMonitorAction +import org.opensearch.alerting.transport.TransportDeleteNoteAction import org.opensearch.alerting.transport.TransportDeleteWorkflowAction import org.opensearch.alerting.transport.TransportDocLevelMonitorFanOutAction import org.opensearch.alerting.transport.TransportExecuteMonitorAction @@ -69,10 +74,12 @@ import org.opensearch.alerting.transport.TransportGetRemoteIndexesAction import org.opensearch.alerting.transport.TransportGetWorkflowAction import org.opensearch.alerting.transport.TransportGetWorkflowAlertsAction import org.opensearch.alerting.transport.TransportIndexMonitorAction +import org.opensearch.alerting.transport.TransportIndexNoteAction import org.opensearch.alerting.transport.TransportIndexWorkflowAction import org.opensearch.alerting.transport.TransportSearchEmailAccountAction import org.opensearch.alerting.transport.TransportSearchEmailGroupAction import org.opensearch.alerting.transport.TransportSearchMonitorAction +import org.opensearch.alerting.transport.TransportSearchNoteAction import org.opensearch.alerting.util.DocLevelMonitorQueries import org.opensearch.alerting.util.destinationmigration.DestinationMigrationCoordinator import org.opensearch.client.Client @@ -157,6 +164,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R lateinit var scheduler: JobScheduler lateinit var sweeper: JobSweeper lateinit var scheduledJobIndices: ScheduledJobIndices + lateinit var alertingNotesIndices: NotesIndices lateinit var docLevelMonitorQueries: DocLevelMonitorQueries lateinit var threadPool: ThreadPool lateinit var alertIndices: AlertIndices @@ -194,6 +202,9 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R RestGetWorkflowAction(), RestDeleteWorkflowAction(), RestGetRemoteIndexesAction(), + RestIndexNoteAction(), + RestSearchNoteAction(), + RestDeleteNoteAction(), ) } @@ -220,6 +231,9 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R ActionPlugin.ActionHandler(AlertingActions.INDEX_WORKFLOW_ACTION_TYPE, TransportIndexWorkflowAction::class.java), ActionPlugin.ActionHandler(AlertingActions.GET_WORKFLOW_ACTION_TYPE, TransportGetWorkflowAction::class.java), ActionPlugin.ActionHandler(AlertingActions.DELETE_WORKFLOW_ACTION_TYPE, TransportDeleteWorkflowAction::class.java), + ActionPlugin.ActionHandler(AlertingActions.INDEX_NOTE_ACTION_TYPE, TransportIndexNoteAction::class.java), + ActionPlugin.ActionHandler(AlertingActions.SEARCH_NOTES_ACTION_TYPE, TransportSearchNoteAction::class.java), + ActionPlugin.ActionHandler(AlertingActions.DELETE_NOTES_ACTION_TYPE, TransportDeleteNoteAction::class.java), ActionPlugin.ActionHandler(ExecuteWorkflowAction.INSTANCE, TransportExecuteWorkflowAction::class.java), ActionPlugin.ActionHandler(GetRemoteIndexesAction.INSTANCE, TransportGetRemoteIndexesAction::class.java), ActionPlugin.ActionHandler(DocLevelMonitorFanOutAction.INSTANCE, TransportDocLevelMonitorFanOutAction::class.java) @@ -278,6 +292,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R .registerConsumers() .registerDestinationSettings() scheduledJobIndices = ScheduledJobIndices(client.admin(), clusterService) + alertingNotesIndices = NotesIndices(environment.settings(), client, threadPool, clusterService) docLevelMonitorQueries = DocLevelMonitorQueries(client, clusterService) scheduler = JobScheduler(threadPool, runner) sweeper = JobSweeper(environment.settings(), client, clusterService, threadPool, xContentRegistry, scheduler, ALERTING_JOB_TYPES) @@ -306,6 +321,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R scheduler, runner, scheduledJobIndices, + alertingNotesIndices, docLevelMonitorQueries, destinationMigrationCoordinator, lockService, @@ -380,7 +396,13 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.FINDING_HISTORY_ROLLOVER_PERIOD, AlertingSettings.FINDING_HISTORY_RETENTION_PERIOD, AlertingSettings.FINDINGS_INDEXING_BATCH_SIZE, - AlertingSettings.CROSS_CLUSTER_MONITORING_ENABLED + AlertingSettings.CROSS_CLUSTER_MONITORING_ENABLED, + AlertingSettings.NOTES_HISTORY_ENABLED, + AlertingSettings.NOTES_HISTORY_MAX_DOCS, + AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE, + AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD, + AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD, + AlertingSettings.MAX_NOTES_PER_NOTIFICATION ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index b1e876291..1d5d5deac 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -23,6 +23,7 @@ import org.opensearch.alerting.opensearchapi.retry import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.opensearchapi.withClosableContext import org.opensearch.alerting.script.BucketLevelTriggerExecutionContext +import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.util.defaultToPerExecutionAction import org.opensearch.alerting.util.getActionExecutionPolicy import org.opensearch.alerting.util.getBucketKeysHash @@ -273,6 +274,9 @@ object BucketLevelMonitorRunner : MonitorRunner() { // to alertsToUpdate to ensure the Alert doc is updated at the end in either case completedAlertsToUpdate.addAll(completedAlerts) + // retrieve max Notes per Alert notification setting + val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) + // All trigger contexts and results should be available at this point since all triggers were evaluated in the main do-while loop val triggerCtx = triggerContexts[trigger.id]!! val triggerResult = triggerResults[trigger.id]!! @@ -291,8 +295,13 @@ object BucketLevelMonitorRunner : MonitorRunner() { for (alertCategory in actionExecutionScope.actionableAlerts) { val alertsToExecuteActionsFor = nextAlerts[trigger.id]?.get(alertCategory) ?: mutableListOf() for (alert in alertsToExecuteActionsFor) { - val alertContext = if (alertCategory != AlertCategory.NEW) AlertContext(alert = alert) - else getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs) + + val alertContext = if (alertCategory != AlertCategory.NEW) { + val alertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(alert.id, maxNotes) + AlertContext(alert = alert, notes = alertNotes) + } else { + getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs) + } val actionCtx = getActionContextForAlertCategory( alertCategory, alertContext, triggerCtx, monitorOrTriggerError @@ -325,11 +334,17 @@ object BucketLevelMonitorRunner : MonitorRunner() { continue val actionCtx = triggerCtx.copy( - dedupedAlerts = dedupedAlerts, + dedupedAlerts = dedupedAlerts.map { + val dedupedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) + AlertContext(alert = it, notes = dedupedAlertsNotes) + }, newAlerts = newAlerts.map { getAlertContext(alert = it, alertSampleDocs = alertSampleDocs) }, - completedAlerts = completedAlerts, + completedAlerts = completedAlerts.map { + val completedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) + AlertContext(alert = it, notes = completedAlertsNotes) + }, error = monitorResult.error ?: triggerResult.error ) val actionResult = this.runAction(action, actionCtx, monitorCtx, monitor, dryrun) @@ -530,11 +545,11 @@ object BucketLevelMonitorRunner : MonitorRunner() { ): BucketLevelTriggerExecutionContext { return when (alertCategory) { AlertCategory.DEDUPED -> - ctx.copy(dedupedAlerts = listOf(alertContext.alert), newAlerts = emptyList(), completedAlerts = emptyList(), error = error) + ctx.copy(dedupedAlerts = listOf(alertContext), newAlerts = emptyList(), completedAlerts = emptyList(), error = error) AlertCategory.NEW -> ctx.copy(dedupedAlerts = emptyList(), newAlerts = listOf(alertContext), completedAlerts = emptyList(), error = error) AlertCategory.COMPLETED -> - ctx.copy(dedupedAlerts = emptyList(), newAlerts = emptyList(), completedAlerts = listOf(alertContext.alert), error = error) + ctx.copy(dedupedAlerts = emptyList(), newAlerts = emptyList(), completedAlerts = listOf(alertContext), error = error) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index b975af728..91cf0ae49 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -6,6 +6,7 @@ package org.opensearch.alerting import org.apache.logging.log4j.LogManager +import org.opensearch.alerting.model.AlertContext import org.opensearch.alerting.model.MonitorRunResult import org.opensearch.alerting.model.QueryLevelTriggerRunResult import org.opensearch.alerting.opensearchapi.InjectorContextElement @@ -16,6 +17,7 @@ import org.opensearch.alerting.util.isADMonitor import org.opensearch.alerting.workflow.WorkflowRunContext import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Monitor +import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.QueryLevelTrigger import org.opensearch.transport.TransportService import java.time.Instant @@ -86,8 +88,22 @@ object QueryLevelMonitorRunner : MonitorRunner() { triggerResults[trigger.id] = triggerResult if (monitorCtx.triggerService!!.isQueryLevelTriggerActionable(triggerCtx, triggerResult, workflowRunContext)) { - val actionCtx = triggerCtx.copy(error = monitorResult.error ?: triggerResult.error) + var actionCtx: QueryLevelTriggerExecutionContext for (action in trigger.actions) { + var currentAlertNotes: List? + if (currentAlert != null) { + // only if an Alert was already active before could it possibly have Notes + val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) + currentAlertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(currentAlert.id, maxNotes) + actionCtx = triggerCtx.copy( + error = monitorResult.error ?: triggerResult.error, + alertContext = AlertContext(alert = currentAlert, notes = currentAlertNotes) + ) + } else { + actionCtx = triggerCtx.copy( + error = monitorResult.error ?: triggerResult.error + ) + } triggerResult.actionResults[action.id] = this.runAction(action, actionCtx, monitorCtx, monitor, dryrun) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index c9b730f1f..a7f9f94fb 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -5,6 +5,9 @@ package org.opensearch.alerting.alerts +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper import org.opensearch.ResourceAlreadyExistsException @@ -19,6 +22,8 @@ import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest import org.opensearch.action.admin.indices.rollover.RolloverRequest import org.opensearch.action.admin.indices.rollover.RolloverResponse +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.IndicesOptions import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.alerting.alerts.AlertIndices.Companion.ALERT_HISTORY_WRITE_INDEX @@ -38,6 +43,7 @@ import org.opensearch.alerting.settings.AlertingSettings.Companion.FINDING_HISTO import org.opensearch.alerting.settings.AlertingSettings.Companion.REQUEST_TIMEOUT import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.IndexUtils +import org.opensearch.alerting.util.NotesUtils import org.opensearch.client.Client import org.opensearch.cluster.ClusterChangedEvent import org.opensearch.cluster.ClusterStateListener @@ -45,13 +51,23 @@ import org.opensearch.cluster.metadata.IndexMetadata import org.opensearch.cluster.service.ClusterService import org.opensearch.common.settings.Settings import org.opensearch.common.unit.TimeValue +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.DataSources import org.opensearch.core.action.ActionListener +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import org.opensearch.index.query.QueryBuilders +import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.threadpool.Scheduler.Cancellable import org.opensearch.threadpool.ThreadPool import java.time.Instant +private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + /** * Class to manage the creation and rollover of alert indices and alert history indices. In progress alerts are stored * in [ALERT_INDEX]. Completed alerts are written to [ALERT_HISTORY_WRITE_INDEX] which is an alias that points at the @@ -186,8 +202,7 @@ class AlertIndices( } catch (e: Exception) { // This should be run on cluster startup logger.error( - "Error creating alert/finding indices. " + - "Alerts/Findings can't be recorded until master node is restarted.", + "Error creating alert/finding indices. Alerts/Findings can't be recorded until master node is restarted.", e ) } @@ -382,7 +397,9 @@ class AlertIndices( } // TODO call getMapping and compare actual mappings here instead of this - if (targetIndex == IndexUtils.lastUpdatedAlertHistoryIndex || targetIndex == IndexUtils.lastUpdatedFindingHistoryIndex) { + if (targetIndex == IndexUtils.lastUpdatedAlertHistoryIndex || + targetIndex == IndexUtils.lastUpdatedFindingHistoryIndex + ) { return } @@ -477,7 +494,6 @@ class AlertIndices( } private fun deleteOldIndices(tag: String, indices: String) { - logger.info("info deleteOldIndices") val clusterStateRequest = ClusterStateRequest() .clear() .indices(indices) @@ -489,9 +505,14 @@ class AlertIndices( object : ActionListener { override fun onResponse(clusterStateResponse: ClusterStateResponse) { if (clusterStateResponse.state.metadata.indices.isNotEmpty()) { - val indicesToDelete = getIndicesToDelete(clusterStateResponse) - logger.info("Deleting old $tag indices viz $indicesToDelete") - deleteAllOldHistoryIndices(indicesToDelete) + scope.launch { + val indicesToDelete = getIndicesToDelete(clusterStateResponse) + logger.info("Deleting old $tag indices viz $indicesToDelete") + if (indices == ALERT_HISTORY_ALL) { + deleteAlertNotes(indicesToDelete) + } + deleteAllOldHistoryIndices(indicesToDelete) + } } else { logger.info("No Old $tag Indices to delete") } @@ -503,7 +524,7 @@ class AlertIndices( ) } - private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { + private suspend fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { val indicesToDelete = mutableListOf() for (entry in clusterStateResponse.state.metadata.indices) { val indexMetaData = entry.value @@ -551,7 +572,8 @@ class AlertIndices( override fun onResponse(deleteIndicesResponse: AcknowledgedResponse) { if (!deleteIndicesResponse.isAcknowledged) { logger.error( - "Could not delete one or more Alerting/Finding history indices: $indicesToDelete. Retrying one by one." + "Could not delete one or more Alerting/Finding history indices: $indicesToDelete." + + "Retrying one by one." ) deleteOldHistoryIndex(indicesToDelete) } @@ -585,4 +607,59 @@ class AlertIndices( ) } } + + private suspend fun deleteAlertNotes(alertHistoryIndicesToDelete: List) { + alertHistoryIndicesToDelete.forEach { alertHistoryIndex -> + val alertIDs = getAlertIDsFromAlertHistoryIndex(alertHistoryIndex) + val notesToDeleteIDs = NotesUtils.getNoteIDsByAlertIDs(client, alertIDs) + NotesUtils.deleteNotes(client, notesToDeleteIDs) + } + } + + private suspend fun getAlertIDsFromAlertHistoryIndex(indexName: String): List { + val queryBuilder = QueryBuilders.matchAllQuery() + val searchSourceBuilder = SearchSourceBuilder() + .query(queryBuilder) + .version(true) + .seqNoAndPrimaryTerm(true) + + val searchRequest = SearchRequest() + .indices(indexName) + .source(searchSourceBuilder) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + val alertIDs = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val alert = Alert.parse(xcp, hit.id, hit.version) + alert.id + } + + return alertIDs.distinct() + + // TODO: could using aggregation search be better? if so figure out how to do it +// val queryBuilder = QueryBuilders.matchAllQuery() +// val searchSourceBuilder = SearchSourceBuilder() +// .size(0) +// .query(queryBuilder) +// .aggregation( +// TermsAggregationBuilder("alert_ids").field("_id").size(10000) // TODO: set to max docs setting +// ) +// .version(true) +// .seqNoAndPrimaryTerm(true) +// +// val searchRequest = SearchRequest() +// .indices(indexName) +// .source(searchSourceBuilder) +// +// val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } +// val alertIDs = searchResponse.aggregations.asMap()["alert_ids"] // how to continue? +// +// return notes + } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt index f981691c8..34dc1689c 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt @@ -7,6 +7,7 @@ package org.opensearch.alerting.model import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.DocLevelQuery +import org.opensearch.commons.alerting.model.Note /** * This model is a wrapper for [Alert] that should only be used to create a more @@ -15,7 +16,8 @@ import org.opensearch.commons.alerting.model.DocLevelQuery data class AlertContext( val alert: Alert, val associatedQueries: List? = null, - val sampleDocs: List>? = null + val sampleDocs: List>? = null, + val notes: List? = null ) { fun asTemplateArg(): Map { val queriesContext = associatedQueries?.map { @@ -26,10 +28,19 @@ data class AlertContext( ) } + val notesContext = notes?.map { + mapOf( + Note.NOTE_TIME_FIELD to it.time, + Note.NOTE_CONTENT_FIELD to it.content, + Note.NOTE_USER_FIELD to it.user + ) + } + // Compile the custom context fields. val customContextFields = mapOf( ASSOCIATED_QUERIES_FIELD to queriesContext, - SAMPLE_DOCS_FIELD to sampleDocs + SAMPLE_DOCS_FIELD to sampleDocs, + NOTES_FIELD to notesContext ) // Get the alert template args @@ -45,5 +56,6 @@ data class AlertContext( companion object { const val ASSOCIATED_QUERIES_FIELD = "associated_queries" const val SAMPLE_DOCS_FIELD = "sample_documents" + const val NOTES_FIELD = "notes" } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt new file mode 100644 index 000000000..d272ce3bb --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt @@ -0,0 +1,431 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.notes + +import org.apache.logging.log4j.LogManager +import org.opensearch.ExceptionsHelper +import org.opensearch.ResourceAlreadyExistsException +import org.opensearch.action.admin.cluster.state.ClusterStateRequest +import org.opensearch.action.admin.cluster.state.ClusterStateResponse +import org.opensearch.action.admin.indices.alias.Alias +import org.opensearch.action.admin.indices.create.CreateIndexRequest +import org.opensearch.action.admin.indices.create.CreateIndexResponse +import org.opensearch.action.admin.indices.delete.DeleteIndexRequest +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse +import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest +import org.opensearch.action.admin.indices.rollover.RolloverRequest +import org.opensearch.action.admin.indices.rollover.RolloverResponse +import org.opensearch.action.support.IndicesOptions +import org.opensearch.action.support.master.AcknowledgedResponse +import org.opensearch.alerting.alerts.AlertIndices +import org.opensearch.alerting.opensearchapi.suspendUntil +import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.util.IndexUtils +import org.opensearch.client.Client +import org.opensearch.cluster.ClusterChangedEvent +import org.opensearch.cluster.ClusterStateListener +import org.opensearch.cluster.metadata.IndexMetadata +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.settings.Settings +import org.opensearch.common.unit.TimeValue +import org.opensearch.common.xcontent.XContentType +import org.opensearch.core.action.ActionListener +import org.opensearch.threadpool.Scheduler +import org.opensearch.threadpool.ThreadPool +import java.time.Instant + +/** + * Initialize the OpenSearch components required to run Notes. + * + */ +class NotesIndices( + settings: Settings, + private val client: Client, + private val threadPool: ThreadPool, + private val clusterService: ClusterService +) : ClusterStateListener { + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_ENABLED) { notesHistoryEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_MAX_DOCS) { notesHistoryMaxDocs = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE) { notesHistoryMaxAge = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD) { + notesHistoryRolloverPeriod = it + rescheduleNotesRollover() + } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD) { + notesHistoryRetentionPeriod = it + } + } + + companion object { +// const val NOTES_INDEX = ".opensearch-alerting-notes" + + /** The alias of the index in which to write notes finding */ + const val NOTES_HISTORY_WRITE_INDEX = ".opensearch-alerting-notes-history-write" + + /** The index name pattern referring to all notes history indices */ + const val NOTES_HISTORY_ALL = ".opensearch-alerting-notes-history*" + + /** The index name pattern to create notes history indices */ + const val NOTES_HISTORY_INDEX_PATTERN = "<.opensearch-alerting-notes-history-{now/d}-1>" + + /** The index name pattern to query all notes, history and current notes. */ + const val ALL_NOTES_INDEX_PATTERN = ".opensearch-alerting-notes*" + + @JvmStatic + fun notesMapping() = + NotesIndices::class.java.getResource("alerting_notes.json").readText() + + private val logger = LogManager.getLogger(AlertIndices::class.java) + } + + @Volatile private var notesHistoryEnabled = AlertingSettings.NOTES_HISTORY_ENABLED.get(settings) + + @Volatile private var notesHistoryMaxDocs = AlertingSettings.NOTES_HISTORY_MAX_DOCS.get(settings) + + @Volatile private var notesHistoryMaxAge = AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE.get(settings) + + @Volatile private var notesHistoryRolloverPeriod = AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD.get(settings) + + @Volatile private var notesHistoryRetentionPeriod = AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD.get(settings) + + @Volatile private var isClusterManager = false + + // for JobsMonitor to report + var lastRolloverTime: TimeValue? = null + + private var notesHistoryIndexInitialized: Boolean = false + + private var scheduledNotesRollover: Scheduler.Cancellable? = null + + /** + * Initialize the indices required for Alerting Notes. + * First check if the index exists, and if not create the index with the provided callback listeners. + * + * @param actionListener A callback listener for the index creation call. Generally in the form of onSuccess, onFailure + */ +// fun initNotesIndex(actionListener: ActionListener) { +// if (!notesIndexExists()) { +// var indexRequest = CreateIndexRequest(NOTES_INDEX) +// .mapping(notesMapping()) +// .settings(Settings.builder().put("index.hidden", true).build()) +// client.indices().create(indexRequest, actionListener) +// } +// } + + fun onMaster() { + try { + // try to rollover immediately as we might be restarting the cluster + rolloverNotesHistoryIndex() + // schedule the next rollover for approx MAX_AGE later + scheduledNotesRollover = threadPool + .scheduleWithFixedDelay({ rolloverAndDeleteNotesHistoryIndices() }, notesHistoryRolloverPeriod, executorName()) + } catch (e: Exception) { + // This should be run on cluster startup + logger.error( + "Error creating notes indices. Notes can't be recorded until master node is restarted.", + e + ) + } + } + + fun offMaster() { + scheduledNotesRollover?.cancel() + } + + private fun executorName(): String { + return ThreadPool.Names.MANAGEMENT + } + + override fun clusterChanged(event: ClusterChangedEvent) { + // Instead of using a LocalNodeClusterManagerListener to track master changes, this service will + // track them here to avoid conditions where master listener events run after other + // listeners that depend on what happened in the master listener + if (this.isClusterManager != event.localNodeClusterManager()) { + this.isClusterManager = event.localNodeClusterManager() + if (this.isClusterManager) { + onMaster() + } else { + offMaster() + } + } + + // if the indexes have been deleted they need to be reinitialized + notesHistoryIndexInitialized = event.state().metadata().hasAlias(NOTES_HISTORY_WRITE_INDEX) + } + + private fun rescheduleNotesRollover() { + if (clusterService.state().nodes.isLocalNodeElectedMaster) { + scheduledNotesRollover?.cancel() + scheduledNotesRollover = threadPool + .scheduleWithFixedDelay({ rolloverAndDeleteNotesHistoryIndices() }, notesHistoryRolloverPeriod, executorName()) + } + } + + fun isNotesHistoryInitialized(): Boolean { + return clusterService.state().metadata.hasAlias(NOTES_HISTORY_WRITE_INDEX) + } + + fun isNotesHistoryEnabled(): Boolean { + return notesHistoryEnabled + } + +// suspend fun createOrUpdateInitialNotesHistoryIndex(dataSources: DataSources) { +// if (dataSources.notesIndex == NotesIndices.NOTES_INDEX) { +// return createOrUpdateInitialNotesHistoryIndex() +// } +// if (!clusterService.state().metadata.hasAlias(dataSources.notesHistoryIndex)) { +// createIndex( +// dataSources.notesHistoryIndexPattern ?: NOTES_HISTORY_INDEX_PATTERN, +// notesMapping(), +// dataSources.notesHistoryIndex +// ) +// } else { +// updateIndexMapping( +// dataSources.notesHistoryIndex ?: NOTES_HISTORY_WRITE_INDEX, +// notesMapping(), +// true +// ) +// } +// } + suspend fun createOrUpdateInitialNotesHistoryIndex() { + if (!isNotesHistoryInitialized()) { + notesHistoryIndexInitialized = createIndex(NOTES_HISTORY_INDEX_PATTERN, notesMapping(), NOTES_HISTORY_WRITE_INDEX) + if (notesHistoryIndexInitialized) + IndexUtils.lastUpdatedNotesHistoryIndex = IndexUtils.getIndexNameWithAlias( + clusterService.state(), + NOTES_HISTORY_WRITE_INDEX + ) + } else { + updateIndexMapping(NOTES_HISTORY_WRITE_INDEX, notesMapping(), true) + } + notesHistoryIndexInitialized + } + + private fun rolloverAndDeleteNotesHistoryIndices() { + rolloverNotesHistoryIndex() + deleteOldIndices("Notes", NOTES_HISTORY_ALL) + } + + private fun rolloverNotesHistoryIndex() { + rolloverIndex( + notesHistoryIndexInitialized, + NOTES_HISTORY_WRITE_INDEX, + NOTES_HISTORY_INDEX_PATTERN, + notesMapping(), + notesHistoryMaxDocs, + notesHistoryMaxAge, + NOTES_HISTORY_WRITE_INDEX + ) + } + + // TODO: Everything below here are util functions straight from AlertIndices.kt + // TODO: might need to reuse their code or refactor + + private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { + val indicesToDelete = mutableListOf() + for (entry in clusterStateResponse.state.metadata.indices) { + val indexMetaData = entry.value + getHistoryIndexToDelete(indexMetaData, notesHistoryRetentionPeriod.millis, NOTES_HISTORY_WRITE_INDEX, true) + ?.let { indicesToDelete.add(it) } + } + return indicesToDelete + } + + private fun getHistoryIndexToDelete( + indexMetadata: IndexMetadata, + retentionPeriodMillis: Long, + writeIndex: String, + historyEnabled: Boolean + ): String? { + val creationTime = indexMetadata.creationDate + if ((Instant.now().toEpochMilli() - creationTime) > retentionPeriodMillis) { + val alias = indexMetadata.aliases.entries.firstOrNull { writeIndex == it.value.alias } + if (alias != null) { + if (historyEnabled) { + // If the index has the write alias and history is enabled, don't delete the index + return null + } else if (writeIndex == NOTES_HISTORY_WRITE_INDEX) { + // Otherwise reset notesHistoryIndexInitialized since index will be deleted + notesHistoryIndexInitialized = false + } + } + + return indexMetadata.index.name + } + return null + } + + private fun deleteAllOldHistoryIndices(indicesToDelete: List) { + if (indicesToDelete.isNotEmpty()) { + val deleteIndexRequest = DeleteIndexRequest(*indicesToDelete.toTypedArray()) + client.admin().indices().delete( + deleteIndexRequest, + object : ActionListener { + override fun onResponse(deleteIndicesResponse: AcknowledgedResponse) { + if (!deleteIndicesResponse.isAcknowledged) { + logger.error( + "Could not delete one or more Notes history indices: $indicesToDelete." + + "Retrying one by one." + ) + deleteOldHistoryIndex(indicesToDelete) + } + } + override fun onFailure(e: Exception) { + logger.error("Delete for Notes History Indices $indicesToDelete Failed. Retrying one By one.") + deleteOldHistoryIndex(indicesToDelete) + } + } + ) + } + } + + private fun deleteOldHistoryIndex(indicesToDelete: List) { + for (index in indicesToDelete) { + val singleDeleteRequest = DeleteIndexRequest(*indicesToDelete.toTypedArray()) + client.admin().indices().delete( + singleDeleteRequest, + object : ActionListener { + override fun onResponse(acknowledgedResponse: AcknowledgedResponse?) { + if (acknowledgedResponse != null) { + if (!acknowledgedResponse.isAcknowledged) { + logger.error("Could not delete one or more Notes history indices: $index") + } + } + } + override fun onFailure(e: Exception) { + logger.debug("Exception ${e.message} while deleting the index $index") + } + } + ) + } + } + + private suspend fun createIndex(index: String, schemaMapping: String, alias: String? = null): Boolean { + // This should be a fast check of local cluster state. Should be exceedingly rare that the local cluster + // state does not contain the index and multiple nodes concurrently try to create the index. + // If it does happen that error is handled we catch the ResourceAlreadyExistsException + val existsResponse: IndicesExistsResponse = client.admin().indices().suspendUntil { + exists(IndicesExistsRequest(index).local(true), it) + } + if (existsResponse.isExists) return true + + logger.debug("index: [$index] schema mappings: [$schemaMapping]") + val request = CreateIndexRequest(index) + .mapping(schemaMapping) + .settings(Settings.builder().put("index.hidden", true).build()) + + if (alias != null) request.alias(Alias(alias)) + return try { + val createIndexResponse: CreateIndexResponse = client.admin().indices().suspendUntil { create(request, it) } + createIndexResponse.isAcknowledged + } catch (t: Exception) { + if (ExceptionsHelper.unwrapCause(t) is ResourceAlreadyExistsException) { + true + } else { + throw t // TODO: wrap in some kind of NotesException? + } + } + } + + private suspend fun updateIndexMapping(index: String, mapping: String, alias: Boolean = false) { + val clusterState = clusterService.state() + var targetIndex = index + if (alias) { + targetIndex = IndexUtils.getIndexNameWithAlias(clusterState, index) + } + + // TODO call getMapping and compare actual mappings here instead of this + if (targetIndex == IndexUtils.lastUpdatedNotesHistoryIndex + ) { + return + } + + val putMappingRequest: PutMappingRequest = PutMappingRequest(targetIndex) + .source(mapping, XContentType.JSON) + val updateResponse: AcknowledgedResponse = client.admin().indices().suspendUntil { putMapping(putMappingRequest, it) } + if (updateResponse.isAcknowledged) { + logger.info("Index mapping of $targetIndex is updated") + setIndexUpdateFlag(index, targetIndex) + } else { + logger.info("Failed to update index mapping of $targetIndex") + } + } + + private fun setIndexUpdateFlag(index: String, targetIndex: String) { + when (index) { + NOTES_HISTORY_WRITE_INDEX -> IndexUtils.lastUpdatedNotesHistoryIndex = targetIndex + } + } + + private fun rolloverIndex( + initialized: Boolean, + index: String, + pattern: String, + map: String, + docsCondition: Long, + ageCondition: TimeValue, + writeIndex: String + ) { + logger.info("in rolloverIndex, initialize: $initialized") + if (!initialized) { + return + } + + logger.info("sending rollover request") + // We have to pass null for newIndexName in order to get Elastic to increment the index count. + val request = RolloverRequest(index, null) + request.createIndexRequest.index(pattern) + .mapping(map) + .settings(Settings.builder().put("index.hidden", true).build()) + request.addMaxIndexDocsCondition(docsCondition) + request.addMaxIndexAgeCondition(ageCondition) + client.admin().indices().rolloverIndex( + request, + object : ActionListener { + override fun onResponse(response: RolloverResponse) { + if (!response.isRolledOver) { + logger.info("$writeIndex not rolled over. Conditions were: ${response.conditionStatus}") + } else { + logger.info("$writeIndex rolled over. Conditions were: ${response.conditionStatus}") + lastRolloverTime = TimeValue.timeValueMillis(threadPool.absoluteTimeInMillis()) + } + } + override fun onFailure(e: Exception) { + logger.error("$writeIndex not roll over failed.") + } + } + ) + } + + private fun deleteOldIndices(tag: String, indices: String) { + val clusterStateRequest = ClusterStateRequest() + .clear() + .indices(indices) + .metadata(true) + .local(true) + .indicesOptions(IndicesOptions.strictExpand()) + client.admin().cluster().state( + clusterStateRequest, + object : ActionListener { + override fun onResponse(clusterStateResponse: ClusterStateResponse) { + if (clusterStateResponse.state.metadata.indices.isNotEmpty()) { + val indicesToDelete = getIndicesToDelete(clusterStateResponse) + logger.info("Deleting old $tag indices viz $indicesToDelete") + deleteAllOldHistoryIndices(indicesToDelete) + } else { + logger.info("No Old $tag Indices to delete") + } + } + override fun onFailure(e: Exception) { + logger.error("Error fetching cluster state") + } + } + ) + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt new file mode 100644 index 000000000..fab9559a9 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt @@ -0,0 +1,53 @@ +package org.opensearch.alerting.resthandler + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.opensearch.alerting.AlertingPlugin +import org.opensearch.client.node.NodeClient +import org.opensearch.commons.alerting.action.AlertingActions +import org.opensearch.commons.alerting.action.DeleteNoteRequest +import org.opensearch.rest.BaseRestHandler +import org.opensearch.rest.RestHandler.ReplacedRoute +import org.opensearch.rest.RestHandler.Route +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestRequest.Method.DELETE +import org.opensearch.rest.action.RestToXContentListener +import java.io.IOException + +private val log: Logger = LogManager.getLogger(RestDeleteMonitorAction::class.java) + +class RestDeleteNoteAction : BaseRestHandler() { + + override fun getName(): String { + return "delete_note_action" + } + + override fun routes(): List { + return listOf() + } + + override fun replacedRoutes(): MutableList { + return mutableListOf( + ReplacedRoute( + DELETE, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}", + DELETE, + "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/notes/{noteID}" + ) + ) + } + + @Throws(IOException::class) + override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}") + + val noteId = request.param("noteID") + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/$noteId") + + val deleteMonitorRequest = DeleteNoteRequest(noteId) + + return RestChannelConsumer { channel -> + client.execute(AlertingActions.DELETE_NOTES_ACTION_TYPE, deleteMonitorRequest, RestToXContentListener(channel)) + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt new file mode 100644 index 000000000..3fa299cf7 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.resthandler + +import org.apache.logging.log4j.LogManager +import org.opensearch.alerting.AlertingPlugin +import org.opensearch.alerting.util.AlertingException +import org.opensearch.alerting.util.IF_PRIMARY_TERM +import org.opensearch.alerting.util.IF_SEQ_NO +import org.opensearch.client.node.NodeClient +import org.opensearch.commons.alerting.action.AlertingActions +import org.opensearch.commons.alerting.action.IndexNoteRequest +import org.opensearch.commons.alerting.action.IndexNoteResponse +import org.opensearch.commons.alerting.model.Alert +import org.opensearch.commons.alerting.model.Note +import org.opensearch.core.rest.RestStatus +import org.opensearch.core.xcontent.ToXContent +import org.opensearch.index.seqno.SequenceNumbers +import org.opensearch.rest.BaseRestHandler +import org.opensearch.rest.BytesRestResponse +import org.opensearch.rest.RestChannel +import org.opensearch.rest.RestHandler.ReplacedRoute +import org.opensearch.rest.RestHandler.Route +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestRequest.Method.POST +import org.opensearch.rest.RestRequest.Method.PUT +import org.opensearch.rest.RestResponse +import org.opensearch.rest.action.RestResponseListener +import java.io.IOException + +private val log = LogManager.getLogger(RestIndexMonitorAction::class.java) + +/** + * Rest handlers to create and update notes. + */ +class RestIndexNoteAction : BaseRestHandler() { + + override fun getName(): String { + return "index_note_action" + } + + override fun routes(): List { + return listOf() + } + + override fun replacedRoutes(): MutableList { + return mutableListOf( + ReplacedRoute( + POST, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/{alertID}/notes", + POST, + "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/{alertID}/notes", + ), + ReplacedRoute( + PUT, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}", + PUT, + "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/notes/{noteID}", + ) + ) + } + + @Throws(IOException::class) + override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes") + + val alertId = request.param("alertID", Alert.NO_ID) + val noteId = request.param("noteID", Note.NO_ID) + if (request.method() == POST && Alert.NO_ID == alertId) { + throw AlertingException.wrap(IllegalArgumentException("Missing alert ID")) + } else if (request.method() == PUT && Note.NO_ID == noteId) { + throw AlertingException.wrap(IllegalArgumentException("Missing note ID")) + } + + // TODO: validation for empty string? + val content = request.contentParser().map()["content"] as String? + ?: throw AlertingException.wrap(IllegalArgumentException("Missing note content")) + val seqNo = request.paramAsLong(IF_SEQ_NO, SequenceNumbers.UNASSIGNED_SEQ_NO) + val primaryTerm = request.paramAsLong(IF_PRIMARY_TERM, SequenceNumbers.UNASSIGNED_PRIMARY_TERM) + + val indexNoteRequest = IndexNoteRequest(alertId, noteId, seqNo, primaryTerm, request.method(), content) + + return RestChannelConsumer { channel -> + client.execute(AlertingActions.INDEX_NOTE_ACTION_TYPE, indexNoteRequest, indexNoteResponse(channel, request.method())) + } + } +} +private fun indexNoteResponse(channel: RestChannel, restMethod: RestRequest.Method): + RestResponseListener { + return object : RestResponseListener(channel) { + @Throws(Exception::class) + override fun buildResponse(response: IndexNoteResponse): RestResponse { + var returnStatus = RestStatus.CREATED + if (restMethod == RestRequest.Method.PUT) + returnStatus = RestStatus.OK + + val restResponse = BytesRestResponse(returnStatus, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + if (returnStatus == RestStatus.CREATED) { + val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/${response.id}" + restResponse.addHeader("Location", location) + } + return restResponse + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt index 5cc9cbd34..8738a86e6 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt @@ -120,6 +120,10 @@ class RestSearchMonitorAction( channel.request().xContentRegistry, LoggingDeprecationHandler.INSTANCE, hit.sourceAsString ).use { hitsParser -> + log.info("monitor hit sourceAsString: ${hit.sourceAsString}") + log.info("monitor parser curr token: ${hitsParser.currentToken()}") + hitsParser.nextToken() + log.info("monitor parser next token: ${hitsParser.currentToken()}") val monitor = ScheduledJob.parse(hitsParser, hit.id, hit.version) val xcb = monitor.toXContent(jsonBuilder(), EMPTY_PARAMS) hit.sourceRef(BytesReference.bytes(xcb)) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt new file mode 100644 index 000000000..2f9a6af47 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.resthandler + +import org.apache.logging.log4j.LogManager +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse +import org.opensearch.alerting.AlertingPlugin +import org.opensearch.alerting.notes.NotesIndices.Companion.ALL_NOTES_INDEX_PATTERN +import org.opensearch.alerting.util.context +import org.opensearch.client.node.NodeClient +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.XContentFactory.jsonBuilder +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.action.AlertingActions +import org.opensearch.commons.alerting.action.SearchNoteRequest +import org.opensearch.commons.alerting.model.Note +import org.opensearch.core.common.bytes.BytesReference +import org.opensearch.core.rest.RestStatus +import org.opensearch.core.xcontent.ToXContent.EMPTY_PARAMS +import org.opensearch.rest.BaseRestHandler +import org.opensearch.rest.BytesRestResponse +import org.opensearch.rest.RestChannel +import org.opensearch.rest.RestHandler.ReplacedRoute +import org.opensearch.rest.RestHandler.Route +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestResponse +import org.opensearch.rest.action.RestResponseListener +import org.opensearch.search.builder.SearchSourceBuilder +import java.io.IOException + +private val log = LogManager.getLogger(RestIndexMonitorAction::class.java) + +/** + * Rest handler to search notes. + */ +class RestSearchNoteAction() : BaseRestHandler() { + + override fun getName(): String { + return "search_notes_action" + } + + override fun routes(): List { + return listOf() + } + + override fun replacedRoutes(): MutableList { + return mutableListOf( + ReplacedRoute( + RestRequest.Method.GET, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/_search", + RestRequest.Method.GET, + "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/notes/_search", + ) + ) + } + + @Throws(IOException::class) + override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/_search") + + val searchSourceBuilder = SearchSourceBuilder() + searchSourceBuilder.parseXContent(request.contentOrSourceParamParser()) + searchSourceBuilder.fetchSource(context(request)) + + val searchRequest = SearchRequest() + .source(searchSourceBuilder) + .indices(ALL_NOTES_INDEX_PATTERN) + + val searchNoteRequest = SearchNoteRequest(searchRequest) + return RestChannelConsumer { channel -> + client.execute(AlertingActions.SEARCH_NOTES_ACTION_TYPE, searchNoteRequest, searchNoteResponse(channel)) + } + } + + private fun searchNoteResponse(channel: RestChannel): RestResponseListener { + return object : RestResponseListener(channel) { + @Throws(Exception::class) + override fun buildResponse(response: SearchResponse): RestResponse { + if (response.isTimedOut) { + return BytesRestResponse(RestStatus.REQUEST_TIMEOUT, response.toString()) + } + + // Swallow exception and return response as is + try { + for (hit in response.hits) { + XContentType.JSON.xContent().createParser( + channel.request().xContentRegistry, + LoggingDeprecationHandler.INSTANCE, + hit.sourceAsString + ).use { hitsParser -> + log.info("hit sourceAsString: ${hit.sourceAsString}") + // at parser's initialization, it points at null, + // need to call nextToken() to get it to point to + // the beginning of the hit object + // TODO: bring this up with team, currently SearchMonitorRestHandler + // doesn't do this, causing the parse to fail, causing search response + // to show objects as is, which bypasses Monitor's toXContent which + // doesn't show User, meaning the Search response shows the whole User + // object, which would be exposure of sensitive information + hitsParser.nextToken() + val note = Note.parse(hitsParser, hit.id) + log.info("note: $note") + val xcb = note.toXContent(jsonBuilder(), EMPTY_PARAMS) + hit.sourceRef(BytesReference.bytes(xcb)) + } + } + } catch (e: Exception) { + log.info("The note parsing failed. Will return response as is.") + } + return BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), EMPTY_PARAMS)) + } + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt index 597ff5b3e..40250d088 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt @@ -9,7 +9,6 @@ import org.apache.logging.log4j.LogManager import org.opensearch.alerting.model.AlertContext import org.opensearch.alerting.model.BucketLevelTriggerRunResult import org.opensearch.alerting.model.MonitorRunResult -import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.BucketLevelTrigger import org.opensearch.commons.alerting.model.Monitor import java.time.Instant @@ -22,9 +21,9 @@ data class BucketLevelTriggerExecutionContext( override val results: List>, override val periodStart: Instant, override val periodEnd: Instant, - val dedupedAlerts: List = listOf(), + val dedupedAlerts: List = listOf(), val newAlerts: List = listOf(), - val completedAlerts: List = listOf(), + val completedAlerts: List = listOf(), override val error: Exception? = null ) : TriggerExecutionContext(monitor, results, periodStart, periodEnd, error) { @@ -32,9 +31,9 @@ data class BucketLevelTriggerExecutionContext( monitor: Monitor, trigger: BucketLevelTrigger, monitorRunResult: MonitorRunResult, - dedupedAlerts: List = listOf(), + dedupedAlerts: List = listOf(), newAlerts: List = listOf(), - completedAlerts: List = listOf() + completedAlerts: List = listOf() ) : this( monitor, trigger, monitorRunResult.inputResults.results, monitorRunResult.periodStart, monitorRunResult.periodEnd, dedupedAlerts, newAlerts, completedAlerts, monitorRunResult.scriptContextError(trigger) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt index 2c7b53097..8a73d6786 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt @@ -5,6 +5,7 @@ package org.opensearch.alerting.script +import org.opensearch.alerting.model.AlertContext import org.opensearch.alerting.model.MonitorRunResult import org.opensearch.alerting.model.QueryLevelTriggerRunResult import org.opensearch.commons.alerting.model.Alert @@ -19,6 +20,7 @@ data class QueryLevelTriggerExecutionContext( override val periodStart: Instant, override val periodEnd: Instant, val alert: Alert? = null, + val alertContext: AlertContext? = null, override val error: Exception? = null ) : TriggerExecutionContext(monitor, results, periodStart, periodEnd, error) { @@ -26,10 +28,11 @@ data class QueryLevelTriggerExecutionContext( monitor: Monitor, trigger: QueryLevelTrigger, monitorRunResult: MonitorRunResult, - alert: Alert? = null + alert: Alert? = null, + alertContext: AlertContext? = null ) : this( monitor, trigger, monitorRunResult.inputResults.results, monitorRunResult.periodStart, monitorRunResult.periodEnd, - alert, monitorRunResult.scriptContextError(trigger) + alert, alertContext, monitorRunResult.scriptContextError(trigger) ) /** @@ -39,7 +42,7 @@ data class QueryLevelTriggerExecutionContext( override fun asTemplateArg(): Map { val tempArg = super.asTemplateArg().toMutableMap() tempArg["trigger"] = trigger.asTemplateArg() - tempArg["alert"] = alert?.asTemplateArg() + tempArg["alert"] = alertContext?.asTemplateArg() // map "alert" templateArg field to alertContext wrapper instead of alert return tempArg } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index f782be9b5..613524a65 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -223,5 +223,43 @@ class AlertingSettings { Int.MAX_VALUE, Setting.Property.NodeScope, Setting.Property.Dynamic ) + + val NOTES_HISTORY_ENABLED = Setting.boolSetting( + "plugins.notes_history_enabled", + true, + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + + val NOTES_HISTORY_MAX_DOCS = Setting.longSetting( + "plugins.notes_history_max_docs", + 1000L, + 0L, + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + + val NOTES_HISTORY_INDEX_MAX_AGE = Setting.positiveTimeSetting( + "plugins.notes_history_max_age", + TimeValue(30, TimeUnit.DAYS), + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + + val NOTES_HISTORY_ROLLOVER_PERIOD = Setting.positiveTimeSetting( + "plugins.notes_history_rollover_period", + TimeValue(12, TimeUnit.HOURS), + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + + val NOTES_HISTORY_RETENTION_PERIOD = Setting.positiveTimeSetting( + "plugins.notes_history_retention_period", + TimeValue(60, TimeUnit.DAYS), + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + + val MAX_NOTES_PER_NOTIFICATION = Setting.intSetting( + "plugins.alerting.notes.max_notes_per_notification", + 3, + 0, + Setting.Property.NodeScope, Setting.Property.Dynamic + ) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt new file mode 100644 index 000000000..da9e214ca --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt @@ -0,0 +1,167 @@ +package org.opensearch.alerting.transport + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.logging.log4j.LogManager +import org.opensearch.OpenSearchStatusException +import org.opensearch.action.ActionRequest +import org.opensearch.action.delete.DeleteRequest +import org.opensearch.action.delete.DeleteResponse +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse +import org.opensearch.action.support.ActionFilters +import org.opensearch.action.support.HandledTransportAction +import org.opensearch.alerting.notes.NotesIndices.Companion.ALL_NOTES_INDEX_PATTERN +import org.opensearch.alerting.opensearchapi.suspendUntil +import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.util.AlertingException +import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.action.AlertingActions +import org.opensearch.commons.alerting.action.DeleteNoteRequest +import org.opensearch.commons.alerting.action.DeleteNoteResponse +import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.authuser.User +import org.opensearch.commons.utils.recreateObject +import org.opensearch.core.action.ActionListener +import org.opensearch.core.rest.RestStatus +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import org.opensearch.index.query.QueryBuilders +import org.opensearch.search.builder.SearchSourceBuilder +import org.opensearch.tasks.Task +import org.opensearch.transport.TransportService + +private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) +private val log = LogManager.getLogger(TransportDeleteNoteAction::class.java) + +class TransportDeleteNoteAction @Inject constructor( + transportService: TransportService, + val client: Client, + actionFilters: ActionFilters, + val clusterService: ClusterService, + settings: Settings, + val xContentRegistry: NamedXContentRegistry +) : HandledTransportAction( + AlertingActions.DELETE_NOTES_ACTION_NAME, transportService, actionFilters, ::DeleteNoteRequest +), + SecureTransportAction { + + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + listenFilterBySettingChange(clusterService) + } + + override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { + val transformedRequest = request as? DeleteNoteRequest + ?: recreateObject(request) { DeleteNoteRequest(it) } + + val user = readUserFromThreadContext(client) +// val deleteRequest = DeleteRequest(ALL_NOTES_INDEX_PATTERN, transformedRequest.noteId) + + if (!validateUserBackendRoles(user, actionListener)) { + return + } + scope.launch { + DeleteNoteHandler( + client, + actionListener, + user, + transformedRequest.noteId + ).resolveUserAndStart() + } + } + + inner class DeleteNoteHandler( + private val client: Client, + private val actionListener: ActionListener, + private val user: User?, + private val noteId: String + ) { + + private var sourceIndex: String? = null + suspend fun resolveUserAndStart() { + try { + val note = getNote() + + if (sourceIndex == null) { + actionListener.onFailure( + AlertingException( + "Could not resolve the index the given Note came from", + RestStatus.INTERNAL_SERVER_ERROR, + IllegalStateException() + ) + ) + } + + // if user is null because security plugin is not installed, anyone can delete any note + // otherwise, only allow note deletion if the deletion requester is the same as the note's author + val canDelete = user == null || user.name == note.user?.name || isAdmin(user) + + val deleteRequest = DeleteRequest(sourceIndex, noteId) + + if (canDelete) { + val deleteResponse = deleteNote(deleteRequest) + actionListener.onResponse(DeleteNoteResponse(deleteResponse.id)) + } else { + actionListener.onFailure( + AlertingException("Not allowed to delete this note!", RestStatus.FORBIDDEN, IllegalStateException()) + ) + } + } catch (t: Exception) { + log.error("Failed to delete note $noteId", t) + actionListener.onFailure(AlertingException.wrap(t)) + } + } + + private suspend fun getNote(): Note { + val queryBuilder = QueryBuilders + .boolQuery() + .must(QueryBuilders.termsQuery("_id", noteId)) + val searchSourceBuilder = + SearchSourceBuilder() + .version(true) + .seqNoAndPrimaryTerm(true) + .query(queryBuilder) + val searchRequest = SearchRequest() + .source(searchSourceBuilder) + .indices(ALL_NOTES_INDEX_PATTERN) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + if (searchResponse.hits.totalHits.value == 0L) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Note with $noteId is not found", RestStatus.NOT_FOUND) + ) + ) + } + val notes = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val note = Note.parse(xcp, hit.id) + sourceIndex = hit.index + note + } + + return notes[0] // we searched on Note ID, there should only be one Note in the List + } + } + + private suspend fun deleteNote(deleteRequest: DeleteRequest): DeleteResponse { + log.debug("Deleting the note with id ${deleteRequest.id()}") + return client.suspendUntil { delete(deleteRequest, it) } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt index 343bac978..d779e1f5d 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt @@ -1051,6 +1051,9 @@ class TransportDocLevelMonitorFanOutAction val docFieldTags = parseSampleDocTags(monitor.triggers) val request = MultiGetRequest() + // TODO: model after this code, notifs r only resent for active alerts, which means + // TODO: ull only ever need to query the one index of active notes, which means u hav a + // TODO: concrete, which means u can copy this and use mget // Perform mGet request in batches. findingToDocPairs.chunked(findingsIndexBatchSize).forEach { batch -> batch.forEach { (findingId, docIdAndIndex) -> diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt new file mode 100644 index 000000000..3ee938475 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt @@ -0,0 +1,415 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.transport + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.logging.log4j.LogManager +import org.opensearch.OpenSearchStatusException +import org.opensearch.action.ActionRequest +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse +import org.opensearch.action.index.IndexRequest +import org.opensearch.action.index.IndexResponse +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse +import org.opensearch.action.support.ActionFilters +import org.opensearch.action.support.HandledTransportAction +import org.opensearch.alerting.alerts.AlertIndices +import org.opensearch.alerting.notes.NotesIndices +import org.opensearch.alerting.notes.NotesIndices.Companion.NOTES_HISTORY_WRITE_INDEX +import org.opensearch.alerting.opensearchapi.suspendUntil +import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT +import org.opensearch.alerting.util.AlertingException +import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.action.AlertingActions +import org.opensearch.commons.alerting.action.IndexNoteRequest +import org.opensearch.commons.alerting.action.IndexNoteResponse +import org.opensearch.commons.alerting.model.Alert +import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.authuser.User +import org.opensearch.commons.utils.recreateObject +import org.opensearch.core.action.ActionListener +import org.opensearch.core.common.io.stream.NamedWriteableRegistry +import org.opensearch.core.rest.RestStatus +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import org.opensearch.index.query.QueryBuilders +import org.opensearch.rest.RestRequest +import org.opensearch.search.builder.SearchSourceBuilder +import org.opensearch.tasks.Task +import org.opensearch.transport.TransportService +import java.time.Instant + +private val log = LogManager.getLogger(TransportIndexMonitorAction::class.java) +private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + +class TransportIndexNoteAction +@Inject +constructor( + transportService: TransportService, + val client: Client, + actionFilters: ActionFilters, + val notesIndices: NotesIndices, + val clusterService: ClusterService, + val settings: Settings, + val xContentRegistry: NamedXContentRegistry, + val namedWriteableRegistry: NamedWriteableRegistry, +) : HandledTransportAction( + AlertingActions.INDEX_NOTE_ACTION_NAME, + transportService, + actionFilters, + ::IndexNoteRequest, +), + SecureTransportAction { + // @Volatile private var maxNotes = AlertingSettings.ALERTING_MAX_NOTES.get(settings) // TODO add this setting +// @Volatile private var requestTimeout = AlertingSettings.ALERTING_MAX_SIZE.get(settings) // TODO add this setting + @Volatile private var indexTimeout = AlertingSettings.INDEX_TIMEOUT.get(settings) + + // Notes don't really use filterBy setting, this is only here to implement SecureTransportAction interface so that we can + // use readUserFromThreadContext() + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { +// clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_NOTES) { maxMonitors = it } +// clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_SIZE) { requestTimeout = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(INDEX_TIMEOUT) { indexTimeout = it } + listenFilterBySettingChange(clusterService) + } + + override fun doExecute( + task: Task, + request: ActionRequest, + actionListener: ActionListener, + ) { + val transformedRequest = + request as? IndexNoteRequest + ?: recreateObject(request, namedWriteableRegistry) { + IndexNoteRequest(it) + } + val user = readUserFromThreadContext(client) + + client.threadPool().threadContext.stashContext().use { + scope.launch { + IndexNoteHandler(client, actionListener, transformedRequest, user).start() + } + } + } + + inner class IndexNoteHandler( + private val client: Client, + private val actionListener: ActionListener, + private val request: IndexNoteRequest, + private val user: User?, + ) { + suspend fun start() { + notesIndices.createOrUpdateInitialNotesHistoryIndex() + prepareNotesIndexing() +// alertingNotesIndices.initNotesIndex( +// object : ActionListener { +// override fun onResponse(response: CreateIndexResponse) { +// onCreateMappingsResponse(response.isAcknowledged) +// } +// +// override fun onFailure(t: Exception) { +// // https://github.com/opensearch-project/alerting/issues/646 +// if (ExceptionsHelper.unwrapCause(t) is ResourceAlreadyExistsException) { +// scope.launch { +// // Wait for the yellow status +// val request = +// ClusterHealthRequest() +// .indices(NOTES_INDEX) +// .waitForYellowStatus() +// val response: ClusterHealthResponse = +// client.suspendUntil { +// execute(ClusterHealthAction.INSTANCE, request, it) +// } +// if (response.isTimedOut) { +// log.error("Workflow creation timeout", t) +// actionListener.onFailure( +// OpenSearchException("Cannot determine that the $NOTES_INDEX index is healthy"), +// ) +// } +// // Retry mapping of workflow +// onCreateMappingsResponse(true) +// } +// } else { +// log.error("Failed to create workflow", t) +// actionListener.onFailure(AlertingException.wrap(t)) +// } +// } +// }, +// ) +// } else if (!IndexUtils.notesIndexUpdated) { +// IndexUtils.updateIndexMapping( +// NOTES_INDEX, +// NotesIndices.notesMapping(), +// clusterService.state(), +// client.admin().indices(), +// object : ActionListener { +// override fun onResponse(response: AcknowledgedResponse) { +// onUpdateMappingsResponse(response) +// } +// +// override fun onFailure(t: Exception) { +// log.error("Failed to create workflow", t) +// actionListener.onFailure(AlertingException.wrap(t)) +// } +// }, +// ) +// } else { +// prepareNotesIndexing() +// } + } + +// private suspend fun onCreateMappingsResponse(isAcknowledged: Boolean) { +// if (isAcknowledged) { +// log.info("Created $NOTES_INDEX with mappings.") +// prepareNotesIndexing() +// IndexUtils.notesIndexUpdated() +// } else { +// log.info("Create $NOTES_INDEX mappings call not acknowledged.") +// actionListener.onFailure( +// AlertingException.wrap( +// OpenSearchStatusException( +// "Create $NOTES_INDEX mappings call not acknowledged", +// RestStatus.INTERNAL_SERVER_ERROR, +// ), +// ), +// ) +// } +// } +// +// private suspend fun onUpdateMappingsResponse(response: AcknowledgedResponse) { +// if (response.isAcknowledged) { +// log.info("Updated $NOTES_INDEX with mappings.") +// IndexUtils.scheduledJobIndexUpdated() +// prepareNotesIndexing() +// } else { +// log.error("Update $NOTES_INDEX mappings call not acknowledged.") +// actionListener.onFailure( +// AlertingException.wrap( +// OpenSearchStatusException( +// "Updated $NOTES_INDEX mappings call not acknowledged.", +// RestStatus.INTERNAL_SERVER_ERROR, +// ), +// ), +// ) +// } +// } + + private suspend fun prepareNotesIndexing() { + // TODO: refactor, create Note object from request here, then pass into updateNote() and indexNote() + if (request.method == RestRequest.Method.PUT) { + updateNote() + } else { + indexNote() + } + } + + private suspend fun indexNote() { + // need to validate the existence of the Alert that user is trying to add Note to. + // Also need to check if user has permissions to add a Note to the passed in Alert. To do this, + // we retrieve the Alert to get its associated monitor user, and use that to + // check if they have permissions to the Monitor that generated the Alert + val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.alertId))) + val searchSourceBuilder = + SearchSourceBuilder() + .version(true) + .seqNoAndPrimaryTerm(true) + .query(queryBuilder) + + // search all alerts, since user might want to create a note + // on a completed alert + val searchRequest = + SearchRequest() + .indices(AlertIndices.ALL_ALERT_INDEX_PATTERN) + .source(searchSourceBuilder) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + val alerts = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val alert = Alert.parse(xcp, hit.id, hit.version) + alert + } + + if (alerts.isEmpty()) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Alert with ID ${request.alertId} is not found", RestStatus.NOT_FOUND), + ) + ) + } + + val alert = alerts[0] // there should only be 1 Alert that matched the request alert ID + log.info("checking user permissions in index note") + checkUserPermissionsWithResource(user, alert.monitorUser, actionListener, "monitor", alert.monitorId) + + val note = Note(alertId = request.alertId, content = request.content, time = Instant.now(), user = user) + + val indexRequest = + IndexRequest(NOTES_HISTORY_WRITE_INDEX) + .source(note.toXContentWithUser(XContentFactory.jsonBuilder())) + .setIfSeqNo(request.seqNo) + .setIfPrimaryTerm(request.primaryTerm) + .timeout(indexTimeout) + + log.info("Creating new note: ${note.toXContentWithUser(XContentFactory.jsonBuilder())}") + + try { + val indexResponse: IndexResponse = client.suspendUntil { client.index(indexRequest, it) } + val failureReasons = checkShardsFailure(indexResponse) + if (failureReasons != null) { + actionListener.onFailure( + AlertingException.wrap(OpenSearchStatusException(failureReasons.toString(), indexResponse.status())), + ) + return + } + + actionListener.onResponse( + IndexNoteResponse(indexResponse.id, indexResponse.seqNo, indexResponse.primaryTerm, note) + ) + } catch (t: Exception) { + actionListener.onFailure(AlertingException.wrap(t)) + } + } + + private suspend fun updateNote() { + val getRequest = GetRequest(NOTES_HISTORY_WRITE_INDEX, request.noteId) + try { + val getResponse: GetResponse = client.suspendUntil { client.get(getRequest, it) } + if (!getResponse.isExists) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Note with ${request.noteId} is not found", RestStatus.NOT_FOUND), + ), + ) + return + } + val xcp = + XContentHelper.createParser( + xContentRegistry, + LoggingDeprecationHandler.INSTANCE, + getResponse.sourceAsBytesRef, + XContentType.JSON, + ) + log.info("curr token: ${xcp.currentToken()}") + xcp.nextToken() + log.info("next token: ${xcp.currentToken()}") + val note = Note.parse(xcp, getResponse.id) + log.info("getResponse.id: ${getResponse.id}") + log.info("note: $note") + onGetNoteResponse(note) + } catch (t: Exception) { + actionListener.onFailure(AlertingException.wrap(t)) + } + } + + private suspend fun onGetNoteResponse(currentNote: Note) { + // TODO: where to update time field with creation or last update time, right now it's declared below, but other APIs seem to + // TODO: declare create/update time in the RestHandler class + +// if (user == null || currentNote.user == null) { +// // security is not installed, editing notes is not allowed +// AlertingException.wrap( +// OpenSearchStatusException( +// "Editing Alerting notes is prohibited when the Security plugin is not installed", +// RestStatus.FORBIDDEN, +// ), +// ) +// } + + // check that the user has permissions to edit the note. user can edit note if + // - user is Admin + // - user is the author of the note + if (user != null && !isAdmin(user) && user.name != currentNote.user?.name) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException( + "Note ${request.noteId} created by ${currentNote.user} " + + "can only be edited by Admin or ${currentNote.user} ", + RestStatus.FORBIDDEN, + ), + ), + ) + } + + // TODO: ensure usage of Instant.now() is consistent with index monitor + // retains everything from the original note except content and time + val requestNote = + Note( + id = currentNote.id, + alertId = currentNote.alertId, + content = request.content, + time = Instant.now(), + user = currentNote.user, + ) + + val indexRequest = + IndexRequest(NOTES_HISTORY_WRITE_INDEX) + .source(requestNote.toXContentWithUser(XContentFactory.jsonBuilder())) + .id(requestNote.id) + .setIfSeqNo(request.seqNo) + .setIfPrimaryTerm(request.primaryTerm) + .timeout(indexTimeout) + + log.info( + "Updating note, ${currentNote.id}, from: " + + "${currentNote.content} to: " + + requestNote.content, + ) + + try { + val indexResponse: IndexResponse = client.suspendUntil { client.index(indexRequest, it) } + val failureReasons = checkShardsFailure(indexResponse) + if (failureReasons != null) { + actionListener.onFailure( + AlertingException.wrap(OpenSearchStatusException(failureReasons.toString(), indexResponse.status())), + ) + return + } + + actionListener.onResponse( + IndexNoteResponse( + indexResponse.id, + indexResponse.seqNo, + indexResponse.primaryTerm, + requestNote, + ), + ) + } catch (t: Exception) { + actionListener.onFailure(AlertingException.wrap(t)) + } + } + + private fun checkShardsFailure(response: IndexResponse): String? { + val failureReasons = StringBuilder() + if (response.shardInfo.failed > 0) { + response.shardInfo.failures.forEach { entry -> + failureReasons.append(entry.reason()) + } + return failureReasons.toString() + } + return null + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt new file mode 100644 index 000000000..138bd2cae --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.transport + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.logging.log4j.LogManager +import org.opensearch.action.ActionRequest +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse +import org.opensearch.action.support.ActionFilters +import org.opensearch.action.support.HandledTransportAction +import org.opensearch.alerting.alerts.AlertIndices.Companion.ALL_ALERT_INDEX_PATTERN +import org.opensearch.alerting.opensearchapi.suspendUntil +import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.util.AlertingException +import org.opensearch.alerting.util.use +import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.action.AlertingActions +import org.opensearch.commons.alerting.action.SearchNoteRequest +import org.opensearch.commons.alerting.model.Alert +import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.authuser.User +import org.opensearch.commons.utils.recreateObject +import org.opensearch.core.action.ActionListener +import org.opensearch.core.common.io.stream.NamedWriteableRegistry +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import org.opensearch.index.query.BoolQueryBuilder +import org.opensearch.index.query.QueryBuilders +import org.opensearch.search.builder.SearchSourceBuilder +import org.opensearch.tasks.Task +import org.opensearch.transport.TransportService +import java.io.IOException + +private val log = LogManager.getLogger(TransportSearchMonitorAction::class.java) +private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + +class TransportSearchNoteAction @Inject constructor( + transportService: TransportService, + val settings: Settings, + val client: Client, + clusterService: ClusterService, + actionFilters: ActionFilters, + val namedWriteableRegistry: NamedWriteableRegistry +) : HandledTransportAction( + AlertingActions.SEARCH_NOTES_ACTION_NAME, transportService, actionFilters, ::SearchRequest +), + SecureTransportAction { + + @Volatile + override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + init { + listenFilterBySettingChange(clusterService) + } + + override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { + val transformedRequest = request as? SearchNoteRequest + ?: recreateObject(request, namedWriteableRegistry) { + SearchNoteRequest(it) + } + + val searchSourceBuilder = transformedRequest.searchRequest.source() + .seqNoAndPrimaryTerm(true) + .version(true) + val queryBuilder = if (searchSourceBuilder.query() == null) BoolQueryBuilder() + else QueryBuilders.boolQuery().must(searchSourceBuilder.query()) + + searchSourceBuilder.query(queryBuilder) + .seqNoAndPrimaryTerm(true) + .version(true) + + val user = readUserFromThreadContext(client) + client.threadPool().threadContext.stashContext().use { + scope.launch { + resolve(transformedRequest, actionListener, user) + } + } + } + + suspend fun resolve(searchNoteRequest: SearchNoteRequest, actionListener: ActionListener, user: User?) { + if (user == null) { + // user is null when: 1/ security is disabled. 2/when user is super-admin. + search(searchNoteRequest.searchRequest, actionListener) + } else if (!doFilterForUser(user)) { + // security is enabled and filterby is disabled. + search(searchNoteRequest.searchRequest, actionListener) + } else { + // security is enabled and filterby is enabled. + try { + log.info("Filtering result by: ${user.backendRoles}") + + // first retrieve all Alert IDs current User can see after filtering by backend roles + val alertIDs = getFilteredAlertIDs(user) + + // then filter the returned Notes based on the Alert IDs they're allowed to see + val queryBuilder = searchNoteRequest.searchRequest.source().query() as BoolQueryBuilder + searchNoteRequest.searchRequest.source().query( + queryBuilder.filter( + QueryBuilders.termsQuery(Note.ALERT_ID_FIELD, alertIDs) + ) + ) + + search(searchNoteRequest.searchRequest, actionListener) + } catch (ex: IOException) { + actionListener.onFailure(AlertingException.wrap(ex)) + } + } + } + + fun search(searchRequest: SearchRequest, actionListener: ActionListener) { + client.search( + searchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + actionListener.onResponse(response) + } + + override fun onFailure(t: Exception) { + actionListener.onFailure(AlertingException.wrap(t)) + } + } + ) + } + + // retrieve the IDs of all Alerts after filtering by current User's + // backend roles + private suspend fun getFilteredAlertIDs(user: User): List { + val queryBuilder = QueryBuilders + .boolQuery() + .filter(QueryBuilders.termsQuery("monitor_user.backend_roles.keyword", user.backendRoles)) + val searchSourceBuilder = + SearchSourceBuilder() + .version(true) + .seqNoAndPrimaryTerm(true) + .query(queryBuilder) + val searchRequest = SearchRequest() + .source(searchSourceBuilder) + .indices(ALL_ALERT_INDEX_PATTERN) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + val alertIDs = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val alert = Alert.parse(xcp, hit.id, hit.version) + alert.id + } + + return alertIDs + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt index f0848aadb..74998fc9a 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt @@ -10,6 +10,7 @@ import org.opensearch.action.support.IndicesOptions import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.alerting.alerts.AlertIndices import org.opensearch.alerting.core.ScheduledJobIndices +import org.opensearch.alerting.notes.NotesIndices import org.opensearch.client.IndicesAdminClient import org.opensearch.cluster.ClusterState import org.opensearch.cluster.metadata.IndexAbstraction @@ -35,6 +36,8 @@ class IndexUtils { private set var findingIndexSchemaVersion: Int private set + var alertingNoteIndexSchemaVersion: Int + private set var scheduledJobIndexUpdated: Boolean = false private set @@ -42,13 +45,17 @@ class IndexUtils { private set var findingIndexUpdated: Boolean = false private set + var notesIndexUpdated: Boolean = false + private set var lastUpdatedAlertHistoryIndex: String? = null var lastUpdatedFindingHistoryIndex: String? = null + var lastUpdatedNotesHistoryIndex: String? = null init { scheduledJobIndexSchemaVersion = getSchemaVersion(ScheduledJobIndices.scheduledJobMappings()) alertIndexSchemaVersion = getSchemaVersion(AlertIndices.alertMapping()) findingIndexSchemaVersion = getSchemaVersion(AlertIndices.findingMapping()) + alertingNoteIndexSchemaVersion = getSchemaVersion(NotesIndices.notesMapping()) } @JvmStatic @@ -66,6 +73,11 @@ class IndexUtils { findingIndexUpdated = true } + @JvmStatic + fun notesIndexUpdated() { + notesIndexUpdated = true + } + @JvmStatic fun getSchemaVersion(mapping: String): Int { val xcp = XContentType.JSON.xContent().createParser( diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt new file mode 100644 index 000000000..e04425a9f --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt @@ -0,0 +1,88 @@ +package org.opensearch.alerting.util + +import org.apache.logging.log4j.LogManager +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse +import org.opensearch.alerting.notes.NotesIndices.Companion.ALL_NOTES_INDEX_PATTERN +import org.opensearch.alerting.opensearchapi.suspendUntil +import org.opensearch.client.Client +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.model.Note +import org.opensearch.core.action.ActionListener +import org.opensearch.core.xcontent.NamedXContentRegistry +import org.opensearch.core.xcontent.XContentParser +import org.opensearch.core.xcontent.XContentParserUtils +import org.opensearch.index.query.QueryBuilders +import org.opensearch.index.reindex.BulkByScrollResponse +import org.opensearch.index.reindex.DeleteByQueryAction +import org.opensearch.index.reindex.DeleteByQueryRequestBuilder +import org.opensearch.search.builder.SearchSourceBuilder +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private val log = LogManager.getLogger(NotesUtils::class.java) + +class NotesUtils { + companion object { + // Deletes all Notes given by the list of Notes IDs + suspend fun deleteNotes(client: Client, noteIDs: List) { + if (noteIDs.isEmpty()) return + val deleteResponse: BulkByScrollResponse = suspendCoroutine { cont -> + DeleteByQueryRequestBuilder(client, DeleteByQueryAction.INSTANCE) + .source(ALL_NOTES_INDEX_PATTERN) + .filter(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", noteIDs))) + .refresh(true) + .execute( + object : ActionListener { + override fun onResponse(response: BulkByScrollResponse) = cont.resume(response) + override fun onFailure(t: Exception) = cont.resumeWithException(t) + } + ) + } + deleteResponse.bulkFailures.forEach { + log.error("Failed to delete Note. Note ID: [${it.id}] cause: [${it.cause}] ") + } + } + + // Searches through all Notes history indices and returns a list of all Notes associated + // with the Entities given by the list of Entity IDs + // TODO: change this to EntityIDs + suspend fun getNotesByAlertIDs(client: Client, alertIDs: List): List { + val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("alert_id", alertIDs)) + val searchSourceBuilder = + SearchSourceBuilder() + .version(true) + .seqNoAndPrimaryTerm(true) + .query(queryBuilder) + + val searchRequest = + SearchRequest() + .indices(ALL_NOTES_INDEX_PATTERN) + .source(searchSourceBuilder) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + val notes = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val note = Note.parse(xcp, hit.id) + note + } + + return notes + } + + // Identical to getNotesByAlertIDs, just returns list of Note IDs instead of list of Note objects + suspend fun getNoteIDsByAlertIDs(client: Client, alertIDs: List): List { + val notes = getNotesByAlertIDs(client, alertIDs) + return notes.map { it.id } + } + } +} diff --git a/alerting/src/main/resources/org/opensearch/alerting/notes/alerting_notes.json b/alerting/src/main/resources/org/opensearch/alerting/notes/alerting_notes.json new file mode 100644 index 000000000..0ba5a305a --- /dev/null +++ b/alerting/src/main/resources/org/opensearch/alerting/notes/alerting_notes.json @@ -0,0 +1,52 @@ +{ + "dynamic": "false", + "properties": { + "alert_id": { + "type": "keyword" + }, + "content": { + "type": "text" + }, + "time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } + } + } +} \ No newline at end of file From 6eb761a4434ae501bcbb16963a13353c109a388b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Sun, 26 May 2024 18:30:59 -0700 Subject: [PATCH 02/22] refactored QueryLevelTriggerExecutionContext to not need both Alert and AlertContext field, but only AlertContext field Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 2 +- .../org/opensearch/alerting/MonitorRunner.kt | 2 +- .../alerting/QueryLevelMonitorRunner.kt | 45 ++++++++++++------- .../org/opensearch/alerting/TriggerService.kt | 2 +- .../QueryLevelTriggerExecutionContext.kt | 7 +-- .../TransportDocLevelMonitorFanOutAction.kt | 2 +- .../alerting/org.opensearch.alerting.txt | 6 ++- 7 files changed, 39 insertions(+), 27 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index 3678f83b3..7256a9eb8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -159,7 +159,7 @@ class AlertService( workflorwRunContext: WorkflowRunContext? ): Alert? { val currentTime = Instant.now() - val currentAlert = ctx.alert + val currentAlert = ctx.alertContext?.alert val updatedActionExecutionResults = mutableListOf() val currentActionIds = mutableSetOf() diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt index 870b39031..68198e1fe 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt @@ -56,7 +56,7 @@ abstract class MonitorRunner { dryrun: Boolean ): ActionRunResult { return try { - if (ctx is QueryLevelTriggerExecutionContext && !MonitorRunnerService.isActionActionable(action, ctx.alert)) { + if (ctx is QueryLevelTriggerExecutionContext && !MonitorRunnerService.isActionActionable(action, ctx.alertContext?.alert)) { return ActionRunResult(action.id, action.name, mapOf(), true, null, null) } val actionOutput = mutableMapOf() diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index 91cf0ae49..313d8afae 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -17,7 +17,6 @@ import org.opensearch.alerting.util.isADMonitor import org.opensearch.alerting.workflow.WorkflowRunContext import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Monitor -import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.QueryLevelTrigger import org.opensearch.transport.TransportService import java.time.Instant @@ -69,7 +68,12 @@ object QueryLevelMonitorRunner : MonitorRunner() { val triggerResults = mutableMapOf() for (trigger in monitor.triggers) { val currentAlert = currentAlerts[trigger] - val triggerCtx = QueryLevelTriggerExecutionContext(monitor, trigger as QueryLevelTrigger, monitorResult, currentAlert) + val currentAlertContext = currentAlert?.let { + val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) + val currentAlertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(currentAlert.id, maxNotes) + AlertContext(alert = currentAlert, notes = currentAlertNotes.ifEmpty { null }) + } + val triggerCtx = QueryLevelTriggerExecutionContext(monitor, trigger as QueryLevelTrigger, monitorResult, currentAlertContext) val triggerResult = when (monitor.monitorType) { Monitor.MonitorType.QUERY_LEVEL_MONITOR -> monitorCtx.triggerService!!.runQueryLevelTrigger(monitor, trigger, triggerCtx) @@ -88,26 +92,33 @@ object QueryLevelMonitorRunner : MonitorRunner() { triggerResults[trigger.id] = triggerResult if (monitorCtx.triggerService!!.isQueryLevelTriggerActionable(triggerCtx, triggerResult, workflowRunContext)) { - var actionCtx: QueryLevelTriggerExecutionContext + val actionCtx = triggerCtx.copy(error = monitorResult.error ?: triggerResult.error) for (action in trigger.actions) { - var currentAlertNotes: List? - if (currentAlert != null) { - // only if an Alert was already active before could it possibly have Notes - val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) - currentAlertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(currentAlert.id, maxNotes) - actionCtx = triggerCtx.copy( - error = monitorResult.error ?: triggerResult.error, - alertContext = AlertContext(alert = currentAlert, notes = currentAlertNotes) - ) - } else { - actionCtx = triggerCtx.copy( - error = monitorResult.error ?: triggerResult.error - ) - } triggerResult.actionResults[action.id] = this.runAction(action, actionCtx, monitorCtx, monitor, dryrun) } } +// if (monitorCtx.triggerService!!.isQueryLevelTriggerActionable(triggerCtx, triggerResult, workflowRunContext)) { +// var actionCtx: QueryLevelTriggerExecutionContext +// for (action in trigger.actions) { +// var currentAlertNotes: List? +// if (currentAlert != null) { +// // only if an Alert was already active before could it possibly have Notes +// val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) +// currentAlertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(currentAlert.id, maxNotes) +// actionCtx = triggerCtx.copy( +// error = monitorResult.error ?: triggerResult.error, +// alertContext = AlertContext(alert = currentAlert, notes = currentAlertNotes) +// ) +// } else { +// actionCtx = triggerCtx.copy( +// error = monitorResult.error ?: triggerResult.error +// ) +// } +// triggerResult.actionResults[action.id] = this.runAction(action, actionCtx, monitorCtx, monitor, dryrun) +// } +// } + val updatedAlert = monitorCtx.alertService!!.composeQueryLevelAlert( triggerCtx, triggerResult, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt index 21ba32475..9262961b4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt @@ -53,7 +53,7 @@ class TriggerService(val scriptService: ScriptService) { ): Boolean { if (workflowRunContext?.auditDelegateMonitorAlerts == true) return false // Suppress actions if the current alert is acknowledged and there are no errors. - val suppress = ctx.alert?.state == Alert.State.ACKNOWLEDGED && result.error == null && ctx.error == null + val suppress = ctx.alertContext?.alert?.state == Alert.State.ACKNOWLEDGED && result.error == null && ctx.error == null return result.triggered && !suppress } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt index 8a73d6786..f24895818 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt @@ -8,7 +8,6 @@ package org.opensearch.alerting.script import org.opensearch.alerting.model.AlertContext import org.opensearch.alerting.model.MonitorRunResult import org.opensearch.alerting.model.QueryLevelTriggerRunResult -import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Monitor import org.opensearch.commons.alerting.model.QueryLevelTrigger import java.time.Instant @@ -19,7 +18,6 @@ data class QueryLevelTriggerExecutionContext( override val results: List>, override val periodStart: Instant, override val periodEnd: Instant, - val alert: Alert? = null, val alertContext: AlertContext? = null, override val error: Exception? = null ) : TriggerExecutionContext(monitor, results, periodStart, periodEnd, error) { @@ -28,11 +26,10 @@ data class QueryLevelTriggerExecutionContext( monitor: Monitor, trigger: QueryLevelTrigger, monitorRunResult: MonitorRunResult, - alert: Alert? = null, alertContext: AlertContext? = null ) : this( monitor, trigger, monitorRunResult.inputResults.results, monitorRunResult.periodStart, monitorRunResult.periodEnd, - alert, alertContext, monitorRunResult.scriptContextError(trigger) + alertContext, monitorRunResult.scriptContextError(trigger) ) /** @@ -42,7 +39,7 @@ data class QueryLevelTriggerExecutionContext( override fun asTemplateArg(): Map { val tempArg = super.asTemplateArg().toMutableMap() tempArg["trigger"] = trigger.asTemplateArg() - tempArg["alert"] = alertContext?.asTemplateArg() // map "alert" templateArg field to alertContext wrapper instead of alert + tempArg["alert"] = alertContext?.asTemplateArg() // map "alert" templateArg field to AlertContext wrapper instead of Alert object return tempArg } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt index d779e1f5d..6e902c676 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt @@ -577,7 +577,7 @@ class TransportDocLevelMonitorFanOutAction dryrun: Boolean ): ActionRunResult { return try { - if (ctx is QueryLevelTriggerExecutionContext && !MonitorRunnerService.isActionActionable(action, ctx.alert)) { + if (ctx is QueryLevelTriggerExecutionContext && !MonitorRunnerService.isActionActionable(action, ctx.alertContext?.alert)) { return ActionRunResult(action.id, action.name, mapOf(), true, null, null) } val actionOutput = mutableMapOf() diff --git a/alerting/src/main/resources/org/opensearch/alerting/org.opensearch.alerting.txt b/alerting/src/main/resources/org/opensearch/alerting/org.opensearch.alerting.txt index bd1f94482..243f4cd60 100644 --- a/alerting/src/main/resources/org/opensearch/alerting/org.opensearch.alerting.txt +++ b/alerting/src/main/resources/org/opensearch/alerting/org.opensearch.alerting.txt @@ -27,7 +27,7 @@ class org.opensearch.alerting.script.QueryLevelTriggerExecutionContext { List getResults() java.time.Instant getPeriodStart() java.time.Instant getPeriodEnd() - Alert getAlert() + AlertContext getAlertContext() Exception getError() } @@ -45,6 +45,10 @@ class org.opensearch.commons.alerting.model.QueryLevelTrigger { List getActions() } +class org.opensearch.alerting.model.AlertContext { + Alert getAlert() +} + class org.opensearch.commons.alerting.model.Alert { String getId() long getVersion() From 745cb62f15f62fb9952b1fb61ed31aafe1c99286 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 May 2024 14:11:40 -0700 Subject: [PATCH 03/22] misc additions and fixes Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 2 +- .../org/opensearch/alerting/AlertingPlugin.kt | 2 + .../alerting/alerts/AlertIndices.kt | 20 -- .../opensearch/alerting/model/AlertContext.kt | 3 +- .../alerting/settings/AlertingSettings.kt | 14 ++ .../transport/TransportIndexNoteAction.kt | 181 +++++------------- .../opensearch/alerting/util/NotesUtils.kt | 2 + 7 files changed, 71 insertions(+), 153 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index 7256a9eb8..a51548794 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -484,7 +484,7 @@ class AlertService( */ suspend fun getNotesForAlertNotification(alertId: String, maxNotes: Int): List { val allNotes = NotesUtils.getNotesByAlertIDs(client, listOf(alertId)) - val sortedNotes = allNotes.sortedByDescending { it.time } + val sortedNotes = allNotes.sortedByDescending { it.createdTime } if (sortedNotes.size <= maxNotes) { return sortedNotes } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index 9e6e7b7b8..ae4fe440c 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -402,6 +402,8 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE, AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD, AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD, + AlertingSettings.NOTES_MAX_CONTENT_SIZE, + AlertingSettings.MAX_NOTES_PER_ALERT, AlertingSettings.MAX_NOTES_PER_NOTIFICATION ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index a7f9f94fb..bc54d1ce4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -641,25 +641,5 @@ class AlertIndices( } return alertIDs.distinct() - - // TODO: could using aggregation search be better? if so figure out how to do it -// val queryBuilder = QueryBuilders.matchAllQuery() -// val searchSourceBuilder = SearchSourceBuilder() -// .size(0) -// .query(queryBuilder) -// .aggregation( -// TermsAggregationBuilder("alert_ids").field("_id").size(10000) // TODO: set to max docs setting -// ) -// .version(true) -// .seqNoAndPrimaryTerm(true) -// -// val searchRequest = SearchRequest() -// .indices(indexName) -// .source(searchSourceBuilder) -// -// val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } -// val alertIDs = searchResponse.aggregations.asMap()["alert_ids"] // how to continue? -// -// return notes } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt index 34dc1689c..eca024d63 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt @@ -30,7 +30,8 @@ data class AlertContext( val notesContext = notes?.map { mapOf( - Note.NOTE_TIME_FIELD to it.time, + Note.NOTE_CREATED_TIME_FIELD to it.createdTime, + Note.NOTE_LAST_UPDATED_TIME_FIELD to it.lastUpdatedTime, Note.NOTE_CONTENT_FIELD to it.content, Note.NOTE_USER_FIELD to it.user ) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index 613524a65..0988df4b5 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -255,6 +255,20 @@ class AlertingSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ) + val NOTES_MAX_CONTENT_SIZE = Setting.longSetting( + "plugins.notes.max_content_size", + 2000L, + 0L, + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + + val MAX_NOTES_PER_ALERT = Setting.longSetting( + "plugins.alerting.notes.max_notes_per_alert", + 500L, + 0L, + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + val MAX_NOTES_PER_NOTIFICATION = Setting.intSetting( "plugins.alerting.notes.max_notes_per_notification", 3, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt index 3ee938475..71a75fde6 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt @@ -25,7 +25,10 @@ import org.opensearch.alerting.notes.NotesIndices.Companion.NOTES_HISTORY_WRITE_ import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT +import org.opensearch.alerting.settings.AlertingSettings.Companion.MAX_NOTES_PER_ALERT +import org.opensearch.alerting.settings.AlertingSettings.Companion.NOTES_MAX_CONTENT_SIZE import org.opensearch.alerting.util.AlertingException +import org.opensearch.alerting.util.NotesUtils import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -52,6 +55,7 @@ import org.opensearch.rest.RestRequest import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.IllegalArgumentException import java.time.Instant private val log = LogManager.getLogger(TransportIndexMonitorAction::class.java) @@ -75,17 +79,16 @@ constructor( ::IndexNoteRequest, ), SecureTransportAction { - // @Volatile private var maxNotes = AlertingSettings.ALERTING_MAX_NOTES.get(settings) // TODO add this setting -// @Volatile private var requestTimeout = AlertingSettings.ALERTING_MAX_SIZE.get(settings) // TODO add this setting - @Volatile private var indexTimeout = AlertingSettings.INDEX_TIMEOUT.get(settings) - // Notes don't really use filterBy setting, this is only here to implement SecureTransportAction interface so that we can - // use readUserFromThreadContext() + @Volatile private var notesMaxContentSize = NOTES_MAX_CONTENT_SIZE.get(settings) + @Volatile private var maxNotesPerAlert = MAX_NOTES_PER_ALERT.get(settings) + @Volatile private var indexTimeout = INDEX_TIMEOUT.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { -// clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_NOTES) { maxMonitors = it } -// clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_SIZE) { requestTimeout = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(NOTES_MAX_CONTENT_SIZE) { notesMaxContentSize = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_NOTES_PER_ALERT) { maxNotesPerAlert = it } clusterService.clusterSettings.addSettingsUpdateConsumer(INDEX_TIMEOUT) { indexTimeout = it } listenFilterBySettingChange(clusterService) } @@ -100,6 +103,17 @@ constructor( ?: recreateObject(request, namedWriteableRegistry) { IndexNoteRequest(it) } + + // validate note content size + if (transformedRequest.content.length > notesMaxContentSize) { + actionListener.onFailure( + AlertingException.wrap( + IllegalArgumentException("Note content exceeds max length of $notesMaxContentSize characters"), + ) + ) + return + } + val user = readUserFromThreadContext(client) client.threadPool().threadContext.stashContext().use { @@ -117,102 +131,6 @@ constructor( ) { suspend fun start() { notesIndices.createOrUpdateInitialNotesHistoryIndex() - prepareNotesIndexing() -// alertingNotesIndices.initNotesIndex( -// object : ActionListener { -// override fun onResponse(response: CreateIndexResponse) { -// onCreateMappingsResponse(response.isAcknowledged) -// } -// -// override fun onFailure(t: Exception) { -// // https://github.com/opensearch-project/alerting/issues/646 -// if (ExceptionsHelper.unwrapCause(t) is ResourceAlreadyExistsException) { -// scope.launch { -// // Wait for the yellow status -// val request = -// ClusterHealthRequest() -// .indices(NOTES_INDEX) -// .waitForYellowStatus() -// val response: ClusterHealthResponse = -// client.suspendUntil { -// execute(ClusterHealthAction.INSTANCE, request, it) -// } -// if (response.isTimedOut) { -// log.error("Workflow creation timeout", t) -// actionListener.onFailure( -// OpenSearchException("Cannot determine that the $NOTES_INDEX index is healthy"), -// ) -// } -// // Retry mapping of workflow -// onCreateMappingsResponse(true) -// } -// } else { -// log.error("Failed to create workflow", t) -// actionListener.onFailure(AlertingException.wrap(t)) -// } -// } -// }, -// ) -// } else if (!IndexUtils.notesIndexUpdated) { -// IndexUtils.updateIndexMapping( -// NOTES_INDEX, -// NotesIndices.notesMapping(), -// clusterService.state(), -// client.admin().indices(), -// object : ActionListener { -// override fun onResponse(response: AcknowledgedResponse) { -// onUpdateMappingsResponse(response) -// } -// -// override fun onFailure(t: Exception) { -// log.error("Failed to create workflow", t) -// actionListener.onFailure(AlertingException.wrap(t)) -// } -// }, -// ) -// } else { -// prepareNotesIndexing() -// } - } - -// private suspend fun onCreateMappingsResponse(isAcknowledged: Boolean) { -// if (isAcknowledged) { -// log.info("Created $NOTES_INDEX with mappings.") -// prepareNotesIndexing() -// IndexUtils.notesIndexUpdated() -// } else { -// log.info("Create $NOTES_INDEX mappings call not acknowledged.") -// actionListener.onFailure( -// AlertingException.wrap( -// OpenSearchStatusException( -// "Create $NOTES_INDEX mappings call not acknowledged", -// RestStatus.INTERNAL_SERVER_ERROR, -// ), -// ), -// ) -// } -// } -// -// private suspend fun onUpdateMappingsResponse(response: AcknowledgedResponse) { -// if (response.isAcknowledged) { -// log.info("Updated $NOTES_INDEX with mappings.") -// IndexUtils.scheduledJobIndexUpdated() -// prepareNotesIndexing() -// } else { -// log.error("Update $NOTES_INDEX mappings call not acknowledged.") -// actionListener.onFailure( -// AlertingException.wrap( -// OpenSearchStatusException( -// "Updated $NOTES_INDEX mappings call not acknowledged.", -// RestStatus.INTERNAL_SERVER_ERROR, -// ), -// ), -// ) -// } -// } - - private suspend fun prepareNotesIndexing() { - // TODO: refactor, create Note object from request here, then pass into updateNote() and indexNote() if (request.method == RestRequest.Method.PUT) { updateNote() } else { @@ -258,13 +176,28 @@ constructor( OpenSearchStatusException("Alert with ID ${request.alertId} is not found", RestStatus.NOT_FOUND), ) ) + return } val alert = alerts[0] // there should only be 1 Alert that matched the request alert ID + + val numNotesOnThisAlert = NotesUtils.getNoteIDsByAlertIDs(client, listOf(alert.id)).size + if (numNotesOnThisAlert >= maxNotesPerAlert) { + actionListener.onFailure( + AlertingException.wrap( + IllegalArgumentException( + "This request would create more than the allowed number of Notes" + + "for this Alert: $maxNotesPerAlert" + ) + ) + ) + return + } + log.info("checking user permissions in index note") checkUserPermissionsWithResource(user, alert.monitorUser, actionListener, "monitor", alert.monitorId) - val note = Note(alertId = request.alertId, content = request.content, time = Instant.now(), user = user) + val note = Note(alertId = request.alertId, content = request.content, createdTime = Instant.now(), user = user) val indexRequest = IndexRequest(NOTES_HISTORY_WRITE_INDEX) @@ -312,12 +245,9 @@ constructor( getResponse.sourceAsBytesRef, XContentType.JSON, ) - log.info("curr token: ${xcp.currentToken()}") xcp.nextToken() - log.info("next token: ${xcp.currentToken()}") val note = Note.parse(xcp, getResponse.id) - log.info("getResponse.id: ${getResponse.id}") - log.info("note: $note") + log.info("note to be updated: $note") onGetNoteResponse(note) } catch (t: Exception) { actionListener.onFailure(AlertingException.wrap(t)) @@ -325,19 +255,6 @@ constructor( } private suspend fun onGetNoteResponse(currentNote: Note) { - // TODO: where to update time field with creation or last update time, right now it's declared below, but other APIs seem to - // TODO: declare create/update time in the RestHandler class - -// if (user == null || currentNote.user == null) { -// // security is not installed, editing notes is not allowed -// AlertingException.wrap( -// OpenSearchStatusException( -// "Editing Alerting notes is prohibited when the Security plugin is not installed", -// RestStatus.FORBIDDEN, -// ), -// ) -// } - // check that the user has permissions to edit the note. user can edit note if // - user is Admin // - user is the author of the note @@ -351,18 +268,20 @@ constructor( ), ), ) + return } - // TODO: ensure usage of Instant.now() is consistent with index monitor - // retains everything from the original note except content and time - val requestNote = - Note( - id = currentNote.id, - alertId = currentNote.alertId, - content = request.content, - time = Instant.now(), - user = currentNote.user, - ) + // retains everything from the original note except content and lastUpdatedTime + val requestNote = currentNote.copy(content = request.content, lastUpdatedTime = Instant.now()) + +// val requestNote = +// Note( +// id = currentNote.id, +// alertId = currentNote.alertId, +// content = request.content, +// time = Instant.now(), +// user = currentNote.user, +// ) val indexRequest = IndexRequest(NOTES_HISTORY_WRITE_INDEX) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt index e04425a9f..400001f98 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt @@ -84,5 +84,7 @@ class NotesUtils { val notes = getNotesByAlertIDs(client, alertIDs) return notes.map { it.id } } + + // TODO: make getNotesByAlertID and getNoteIDsByAlertID } } From 85ba7517ad3ee36462aadfdb8fafb35e698b3561 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 27 May 2024 16:27:38 -0700 Subject: [PATCH 04/22] misc cleanup Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 18 ---------------- .../org/opensearch/alerting/AlertingPlugin.kt | 6 +++--- .../alerting/BucketLevelMonitorRunner.kt | 14 +++++++------ .../alerting/QueryLevelMonitorRunner.kt | 21 ------------------- .../alerting/alerts/NotesIndicesIT.kt | 4 ++++ .../resthandler/AlertingNotesRestApiIT.kt | 4 ++++ 6 files changed, 19 insertions(+), 48 deletions(-) create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index a51548794..58d6cbdea 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -746,24 +746,6 @@ class AlertService( throw IllegalStateException("Unexpected attempt to save ${alert.state} alert: $alert") } Alert.State.COMPLETED -> { -// val requestsList = mutableListOf>( -// DeleteRequest(alertsIndex, alert.id) -// .routing(routingId) -// ) -// // Only add completed alert to respective history indices if history is enabled -// if (alertIndices.isAlertHistoryEnabled()) { -// requestsList.add( -// IndexRequest(alertsHistoryIndex) -// .routing(routingId) -// .source(alert.toXContentWithUser(XContentFactory.jsonBuilder())) -// .id(alert.id) -// ) -// } else { -// // Prepare Alert's Notes for deletion as well -// val notes = NotesUtils.searchNotesByAlertID(client, listOf(alert.id)) -// notes.forEach { notesToDeleteIDs.add(it.id) } -// } -// Collections.unmodifiableList(requestsList) listOfNotNull>( DeleteRequest(alertsIndex, alert.id) .routing(routingId), diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index ae4fe440c..018d46067 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -164,7 +164,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R lateinit var scheduler: JobScheduler lateinit var sweeper: JobSweeper lateinit var scheduledJobIndices: ScheduledJobIndices - lateinit var alertingNotesIndices: NotesIndices + lateinit var notesIndices: NotesIndices lateinit var docLevelMonitorQueries: DocLevelMonitorQueries lateinit var threadPool: ThreadPool lateinit var alertIndices: AlertIndices @@ -292,7 +292,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R .registerConsumers() .registerDestinationSettings() scheduledJobIndices = ScheduledJobIndices(client.admin(), clusterService) - alertingNotesIndices = NotesIndices(environment.settings(), client, threadPool, clusterService) + notesIndices = NotesIndices(environment.settings(), client, threadPool, clusterService) docLevelMonitorQueries = DocLevelMonitorQueries(client, clusterService) scheduler = JobScheduler(threadPool, runner) sweeper = JobSweeper(environment.settings(), client, clusterService, threadPool, xContentRegistry, scheduler, ALERTING_JOB_TYPES) @@ -321,7 +321,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R scheduler, runner, scheduledJobIndices, - alertingNotesIndices, + notesIndices, docLevelMonitorQueries, destinationMigrationCoordinator, lockService, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index 1d5d5deac..c6f6e2c75 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -59,6 +59,7 @@ import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.transport.TransportService import java.time.Instant import java.util.UUID +import org.opensearch.commons.alerting.model.Note object BucketLevelMonitorRunner : MonitorRunner() { private val logger = LogManager.getLogger(javaClass) @@ -295,12 +296,11 @@ object BucketLevelMonitorRunner : MonitorRunner() { for (alertCategory in actionExecutionScope.actionableAlerts) { val alertsToExecuteActionsFor = nextAlerts[trigger.id]?.get(alertCategory) ?: mutableListOf() for (alert in alertsToExecuteActionsFor) { - + val alertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(alert.id, maxNotes) val alertContext = if (alertCategory != AlertCategory.NEW) { - val alertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(alert.id, maxNotes) AlertContext(alert = alert, notes = alertNotes) } else { - getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs) + getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, alertNotes) } val actionCtx = getActionContextForAlertCategory( @@ -339,7 +339,8 @@ object BucketLevelMonitorRunner : MonitorRunner() { AlertContext(alert = it, notes = dedupedAlertsNotes) }, newAlerts = newAlerts.map { - getAlertContext(alert = it, alertSampleDocs = alertSampleDocs) + val newAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) + getAlertContext(alert = it, alertSampleDocs = alertSampleDocs, alertNotes = newAlertsNotes) }, completedAlerts = completedAlerts.map { val completedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) @@ -555,7 +556,8 @@ object BucketLevelMonitorRunner : MonitorRunner() { private fun getAlertContext( alert: Alert, - alertSampleDocs: Map>>> + alertSampleDocs: Map>>>, + alertNotes: List ): AlertContext { val bucketKey = alert.aggregationResultBucket?.getBucketKeysHash() val sampleDocs = alertSampleDocs[alert.triggerId]?.get(bucketKey) @@ -569,7 +571,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { alert.monitorId, alert.executionId ) - AlertContext(alert = alert, sampleDocs = listOf()) + AlertContext(alert = alert, sampleDocs = listOf(), notes = alertNotes) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index 313d8afae..0a8be3ace 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -98,27 +98,6 @@ object QueryLevelMonitorRunner : MonitorRunner() { } } -// if (monitorCtx.triggerService!!.isQueryLevelTriggerActionable(triggerCtx, triggerResult, workflowRunContext)) { -// var actionCtx: QueryLevelTriggerExecutionContext -// for (action in trigger.actions) { -// var currentAlertNotes: List? -// if (currentAlert != null) { -// // only if an Alert was already active before could it possibly have Notes -// val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) -// currentAlertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(currentAlert.id, maxNotes) -// actionCtx = triggerCtx.copy( -// error = monitorResult.error ?: triggerResult.error, -// alertContext = AlertContext(alert = currentAlert, notes = currentAlertNotes) -// ) -// } else { -// actionCtx = triggerCtx.copy( -// error = monitorResult.error ?: triggerResult.error -// ) -// } -// triggerResult.actionResults[action.id] = this.runAction(action, actionCtx, monitorCtx, monitor, dryrun) -// } -// } - val updatedAlert = monitorCtx.alertService!!.composeQueryLevelAlert( triggerCtx, triggerResult, diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt new file mode 100644 index 000000000..7d84817c5 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt @@ -0,0 +1,4 @@ +package org.opensearch.alerting.alerts + +class NotesIndicesIT { +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt new file mode 100644 index 000000000..0d4789b5c --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt @@ -0,0 +1,4 @@ +package org.opensearch.alerting.resthandler + +class AlertingNotesRestApiIT { +} From 1569cca432c2e9ff3afd5d298078a0b14ef1791b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 29 May 2024 11:52:38 -0700 Subject: [PATCH 05/22] misc changes and basic ITs Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertingPlugin.kt | 1 + .../alerting/BucketLevelMonitorRunner.kt | 2 +- .../alerting/alerts/AlertIndices.kt | 2 +- .../opensearch/alerting/notes/NotesIndices.kt | 28 ---- .../resthandler/RestIndexNoteAction.kt | 8 +- .../resthandler/RestSearchMonitorAction.kt | 4 - .../alerting/settings/AlertingSettings.kt | 18 ++- .../transport/TransportDeleteNoteAction.kt | 13 +- .../transport/TransportIndexNoteAction.kt | 19 ++- .../transport/TransportSearchNoteAction.kt | 19 ++- .../alerting/AlertingRestTestCase.kt | 33 ++++ .../org/opensearch/alerting/TestHelpers.kt | 1 + .../alerting/alerts/NotesIndicesIT.kt | 6 +- .../resthandler/AlertingNotesRestApiIT.kt | 143 +++++++++++++++++- .../alerting/resthandler/MonitorRestApiIT.kt | 3 + .../SecureAlertingNotesRestApiIT.kt | 3 + 16 files changed, 250 insertions(+), 53 deletions(-) create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index 018d46067..d960f1fe8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -397,6 +397,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.FINDING_HISTORY_RETENTION_PERIOD, AlertingSettings.FINDINGS_INDEXING_BATCH_SIZE, AlertingSettings.CROSS_CLUSTER_MONITORING_ENABLED, + AlertingSettings.ALERTING_NOTES_ENABLED, AlertingSettings.NOTES_HISTORY_ENABLED, AlertingSettings.NOTES_HISTORY_MAX_DOCS, AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index c6f6e2c75..44828f3a9 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -39,6 +39,7 @@ import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.BucketLevelTrigger import org.opensearch.commons.alerting.model.Finding import org.opensearch.commons.alerting.model.Monitor +import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.SearchInput import org.opensearch.commons.alerting.model.action.AlertCategory import org.opensearch.commons.alerting.model.action.PerAlertActionScope @@ -59,7 +60,6 @@ import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.transport.TransportService import java.time.Instant import java.util.UUID -import org.opensearch.commons.alerting.model.Note object BucketLevelMonitorRunner : MonitorRunner() { private val logger = LogManager.getLogger(javaClass) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index bc54d1ce4..70b81d4e4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -524,7 +524,7 @@ class AlertIndices( ) } - private suspend fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { + private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { val indicesToDelete = mutableListOf() for (entry in clusterStateResponse.state.metadata.indices) { val indexMetaData = entry.value diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt index d272ce3bb..9c690aa63 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt @@ -63,8 +63,6 @@ class NotesIndices( } companion object { -// const val NOTES_INDEX = ".opensearch-alerting-notes" - /** The alias of the index in which to write notes finding */ const val NOTES_HISTORY_WRITE_INDEX = ".opensearch-alerting-notes-history-write" @@ -109,14 +107,6 @@ class NotesIndices( * * @param actionListener A callback listener for the index creation call. Generally in the form of onSuccess, onFailure */ -// fun initNotesIndex(actionListener: ActionListener) { -// if (!notesIndexExists()) { -// var indexRequest = CreateIndexRequest(NOTES_INDEX) -// .mapping(notesMapping()) -// .settings(Settings.builder().put("index.hidden", true).build()) -// client.indices().create(indexRequest, actionListener) -// } -// } fun onMaster() { try { @@ -175,24 +165,6 @@ class NotesIndices( return notesHistoryEnabled } -// suspend fun createOrUpdateInitialNotesHistoryIndex(dataSources: DataSources) { -// if (dataSources.notesIndex == NotesIndices.NOTES_INDEX) { -// return createOrUpdateInitialNotesHistoryIndex() -// } -// if (!clusterService.state().metadata.hasAlias(dataSources.notesHistoryIndex)) { -// createIndex( -// dataSources.notesHistoryIndexPattern ?: NOTES_HISTORY_INDEX_PATTERN, -// notesMapping(), -// dataSources.notesHistoryIndex -// ) -// } else { -// updateIndexMapping( -// dataSources.notesHistoryIndex ?: NOTES_HISTORY_WRITE_INDEX, -// notesMapping(), -// true -// ) -// } -// } suspend fun createOrUpdateInitialNotesHistoryIndex() { if (!isNotesHistoryInitialized()) { notesHistoryIndexInitialized = createIndex(NOTES_HISTORY_INDEX_PATTERN, notesMapping(), NOTES_HISTORY_WRITE_INDEX) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt index 3fa299cf7..11b297770 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt @@ -98,10 +98,10 @@ private fun indexNoteResponse(channel: RestChannel, restMethod: RestRequest.Meth returnStatus = RestStatus.OK val restResponse = BytesRestResponse(returnStatus, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) - if (returnStatus == RestStatus.CREATED) { - val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/${response.id}" - restResponse.addHeader("Location", location) - } +// if (returnStatus == RestStatus.CREATED) { +// val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/${response.id}" +// restResponse.addHeader("Location", location) +// } return restResponse } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt index 8738a86e6..5cc9cbd34 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt @@ -120,10 +120,6 @@ class RestSearchMonitorAction( channel.request().xContentRegistry, LoggingDeprecationHandler.INSTANCE, hit.sourceAsString ).use { hitsParser -> - log.info("monitor hit sourceAsString: ${hit.sourceAsString}") - log.info("monitor parser curr token: ${hitsParser.currentToken()}") - hitsParser.nextToken() - log.info("monitor parser next token: ${hitsParser.currentToken()}") val monitor = ScheduledJob.parse(hitsParser, hit.id, hit.version) val xcb = monitor.toXContent(jsonBuilder(), EMPTY_PARAMS) hit.sourceRef(BytesReference.bytes(xcb)) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index 0988df4b5..0ce4924ff 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -224,39 +224,45 @@ class AlertingSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ) + val ALERTING_NOTES_ENABLED = Setting.boolSetting( + "plugins.alerting.notes_enabled", + false, + Setting.Property.NodeScope, Setting.Property.Dynamic + ) + val NOTES_HISTORY_ENABLED = Setting.boolSetting( - "plugins.notes_history_enabled", + "plugins.alerting.notes_history_enabled", true, Setting.Property.NodeScope, Setting.Property.Dynamic ) val NOTES_HISTORY_MAX_DOCS = Setting.longSetting( - "plugins.notes_history_max_docs", + "plugins.alerting.notes_history_max_docs", 1000L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic ) val NOTES_HISTORY_INDEX_MAX_AGE = Setting.positiveTimeSetting( - "plugins.notes_history_max_age", + "plugins.alerting.notes_history_max_age", TimeValue(30, TimeUnit.DAYS), Setting.Property.NodeScope, Setting.Property.Dynamic ) val NOTES_HISTORY_ROLLOVER_PERIOD = Setting.positiveTimeSetting( - "plugins.notes_history_rollover_period", + "plugins.alerting.notes_history_rollover_period", TimeValue(12, TimeUnit.HOURS), Setting.Property.NodeScope, Setting.Property.Dynamic ) val NOTES_HISTORY_RETENTION_PERIOD = Setting.positiveTimeSetting( - "plugins.notes_history_retention_period", + "plugins.alerting.notes_history_retention_period", TimeValue(60, TimeUnit.DAYS), Setting.Property.NodeScope, Setting.Property.Dynamic ) val NOTES_MAX_CONTENT_SIZE = Setting.longSetting( - "plugins.notes.max_content_size", + "plugins.alerting.notes.max_content_size", 2000L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt index da9e214ca..35345a678 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt @@ -54,18 +54,29 @@ class TransportDeleteNoteAction @Inject constructor( ), SecureTransportAction { + @Volatile private var alertingNotesEnabled = AlertingSettings.ALERTING_NOTES_ENABLED.get(settings) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_NOTES_ENABLED) { alertingNotesEnabled = it } listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { + // validate feature flag enabled + if (!alertingNotesEnabled) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Notes for Alerting is currently disabled", RestStatus.FORBIDDEN), + ) + ) + return + } + val transformedRequest = request as? DeleteNoteRequest ?: recreateObject(request) { DeleteNoteRequest(it) } val user = readUserFromThreadContext(client) -// val deleteRequest = DeleteRequest(ALL_NOTES_INDEX_PATTERN, transformedRequest.noteId) if (!validateUserBackendRoles(user, actionListener)) { return diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt index 71a75fde6..ffd213a2f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt @@ -24,6 +24,7 @@ import org.opensearch.alerting.notes.NotesIndices import org.opensearch.alerting.notes.NotesIndices.Companion.NOTES_HISTORY_WRITE_INDEX import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_NOTES_ENABLED import org.opensearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT import org.opensearch.alerting.settings.AlertingSettings.Companion.MAX_NOTES_PER_ALERT import org.opensearch.alerting.settings.AlertingSettings.Companion.NOTES_MAX_CONTENT_SIZE @@ -80,6 +81,7 @@ constructor( ), SecureTransportAction { + @Volatile private var alertingNotesEnabled = ALERTING_NOTES_ENABLED.get(settings) @Volatile private var notesMaxContentSize = NOTES_MAX_CONTENT_SIZE.get(settings) @Volatile private var maxNotesPerAlert = MAX_NOTES_PER_ALERT.get(settings) @Volatile private var indexTimeout = INDEX_TIMEOUT.get(settings) @@ -87,6 +89,7 @@ constructor( @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { + clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_NOTES_ENABLED) { alertingNotesEnabled = it } clusterService.clusterSettings.addSettingsUpdateConsumer(NOTES_MAX_CONTENT_SIZE) { notesMaxContentSize = it } clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_NOTES_PER_ALERT) { maxNotesPerAlert = it } clusterService.clusterSettings.addSettingsUpdateConsumer(INDEX_TIMEOUT) { indexTimeout = it } @@ -98,6 +101,16 @@ constructor( request: ActionRequest, actionListener: ActionListener, ) { + // validate feature flag enabled + if (!alertingNotesEnabled) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Notes for Alerting is currently disabled", RestStatus.FORBIDDEN), + ) + ) + return + } + val transformedRequest = request as? IndexNoteRequest ?: recreateObject(request, namedWriteableRegistry) { @@ -143,7 +156,7 @@ constructor( // Also need to check if user has permissions to add a Note to the passed in Alert. To do this, // we retrieve the Alert to get its associated monitor user, and use that to // check if they have permissions to the Monitor that generated the Alert - val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.alertId))) + val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.entityId))) val searchSourceBuilder = SearchSourceBuilder() .version(true) @@ -173,7 +186,7 @@ constructor( if (alerts.isEmpty()) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Alert with ID ${request.alertId} is not found", RestStatus.NOT_FOUND), + OpenSearchStatusException("Alert with ID ${request.entityId} is not found", RestStatus.NOT_FOUND), ) ) return @@ -197,7 +210,7 @@ constructor( log.info("checking user permissions in index note") checkUserPermissionsWithResource(user, alert.monitorUser, actionListener, "monitor", alert.monitorId) - val note = Note(alertId = request.alertId, content = request.content, createdTime = Instant.now(), user = user) + val note = Note(entityId = request.entityId, content = request.content, createdTime = Instant.now(), user = user) val indexRequest = IndexRequest(NOTES_HISTORY_WRITE_INDEX) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt index 138bd2cae..774e924a4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager +import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionRequest import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse @@ -34,6 +35,7 @@ import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener import org.opensearch.core.common.io.stream.NamedWriteableRegistry +import org.opensearch.core.rest.RestStatus import org.opensearch.core.xcontent.NamedXContentRegistry import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParserUtils @@ -59,13 +61,24 @@ class TransportSearchNoteAction @Inject constructor( ), SecureTransportAction { - @Volatile - override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile private var alertingNotesEnabled = AlertingSettings.ALERTING_NOTES_ENABLED.get(settings) + @Volatile override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_NOTES_ENABLED) { alertingNotesEnabled = it } listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { + // validate feature flag enabled + if (!alertingNotesEnabled) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Notes for Alerting is currently disabled", RestStatus.FORBIDDEN), + ) + ) + return + } + val transformedRequest = request as? SearchNoteRequest ?: recreateObject(request, namedWriteableRegistry) { SearchNoteRequest(it) @@ -108,7 +121,7 @@ class TransportSearchNoteAction @Inject constructor( val queryBuilder = searchNoteRequest.searchRequest.source().query() as BoolQueryBuilder searchNoteRequest.searchRequest.source().query( queryBuilder.filter( - QueryBuilders.termsQuery(Note.ALERT_ID_FIELD, alertIDs) + QueryBuilders.termsQuery(Note.ENTITY_ID_FIELD, alertIDs) ) ) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index c7d548c42..abd813f59 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -51,11 +51,13 @@ import org.opensearch.commons.alerting.model.DocumentLevelTrigger import org.opensearch.commons.alerting.model.Finding import org.opensearch.commons.alerting.model.FindingWithDocs import org.opensearch.commons.alerting.model.Monitor +import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.QueryLevelTrigger import org.opensearch.commons.alerting.model.ScheduledJob import org.opensearch.commons.alerting.model.SearchInput import org.opensearch.commons.alerting.model.Workflow import org.opensearch.commons.alerting.util.string +import org.opensearch.commons.authuser.User import org.opensearch.core.rest.RestStatus import org.opensearch.core.xcontent.NamedXContentRegistry import org.opensearch.core.xcontent.ToXContent @@ -521,6 +523,37 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { return alert.copy(id = alertJson["_id"] as String, version = (alertJson["_version"] as Int).toLong()) } + protected fun createAlertNote(alertId: String, content: String): Note { + val createRequestBody = jsonBuilder() + .startObject() + .field(Note.NOTE_CONTENT_FIELD, content) + .endObject() + .string() + + val createResponse = client().makeRequest( + "POST", + "$ALERTING_BASE_URI/alerts/$alertId/notes", + StringEntity(createRequestBody, APPLICATION_JSON) + ) + + assertEquals("Unable to create a new alert", RestStatus.CREATED, createResponse.restStatus()) + + val responseBody = createResponse.asMap() + val noteId = responseBody["_id"] as String + assertNotEquals("response is missing Id", Note.NO_ID, noteId) + + val note = responseBody["note"] as Map<*, *> + + return Note( + id = noteId, + entityId = note["entity_id"] as String, + content = note["content"] as String, + createdTime = Instant.ofEpochMilli(note["created_time"] as Long), + lastUpdatedTime = if (note["last_updated_time"] != null) Instant.ofEpochMilli(note["last_updated_time"] as Long) else null, + user = note["user"]?.let { User(it as String, emptyList(), emptyList(), emptyList()) } + ) + } + protected fun createRandomMonitor(refresh: Boolean = false, withMetadata: Boolean = false): Monitor { val monitor = randomQueryLevelMonitor(withMetadata = withMetadata) val monitorId = createMonitor(monitor, refresh).id diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index 6a66579ef..5367ac015 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -402,6 +402,7 @@ val WORKFLOW_ALERTING_BASE_URI = "/_plugins/_alerting/workflows" val DESTINATION_BASE_URI = "/_plugins/_alerting/destinations" val LEGACY_OPENDISTRO_ALERTING_BASE_URI = "/_opendistro/_alerting/monitors" val LEGACY_OPENDISTRO_DESTINATION_BASE_URI = "/_opendistro/_alerting/destinations" +val ALERTING_NOTES_BASE_URI = "" val ALWAYS_RUN = Script("return true") val NEVER_RUN = Script("return false") val DRYRUN_MONITOR = mapOf("dryrun" to "true") diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt index 7d84817c5..b882d412e 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt @@ -1,4 +1,8 @@ package org.opensearch.alerting.alerts -class NotesIndicesIT { +import org.opensearch.alerting.AlertingRestTestCase + +class NotesIndicesIT : AlertingRestTestCase() { + fun `test create initial notes index`() { + } } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt index 0d4789b5c..2b306211c 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt @@ -1,4 +1,145 @@ package org.opensearch.alerting.resthandler -class AlertingNotesRestApiIT { +import org.apache.hc.core5.http.ContentType +import org.apache.hc.core5.http.io.entity.StringEntity +import org.opensearch.alerting.ALERTING_BASE_URI +import org.opensearch.alerting.AlertingRestTestCase +import org.opensearch.alerting.makeRequest +import org.opensearch.alerting.randomAlert +import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_NOTES_ENABLED +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.model.Alert +import org.opensearch.commons.alerting.model.Note.Companion.NOTE_CONTENT_FIELD +import org.opensearch.commons.alerting.util.string +import org.opensearch.core.rest.RestStatus +import org.opensearch.index.query.QueryBuilders +import org.opensearch.search.builder.SearchSourceBuilder +import org.opensearch.test.OpenSearchTestCase +import org.opensearch.test.junit.annotations.TestLogging +import java.util.concurrent.TimeUnit + +@TestLogging("level:DEBUG", reason = "Debug for tests.") +@Suppress("UNCHECKED_CAST") +class AlertingNotesRestApiIT : AlertingRestTestCase() { + + fun `test creating note`() { + client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val noteContent = "test note" + + val note = createAlertNote(alertId, noteContent) + + assertEquals("Note does not have correct content", noteContent, note.content) + assertEquals("Note does not have correct alert ID", alertId, note.entityId) + } + + fun `test updating note`() { + client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val noteContent = "test note" + + val noteId = createAlertNote(alertId, noteContent).id + + val updateContent = "updated note" + val updateRequestBody = XContentFactory.jsonBuilder() + .startObject() + .field(NOTE_CONTENT_FIELD, updateContent) + .endObject() + .string() + + val updateResponse = client().makeRequest( + "PUT", + "$ALERTING_BASE_URI/alerts/notes/$noteId", + StringEntity(updateRequestBody, ContentType.APPLICATION_JSON) + ) + + assertEquals("Update note failed", RestStatus.OK, updateResponse.restStatus()) + + val updateResponseBody = updateResponse.asMap() + + val note = updateResponseBody["note"] as Map<*, *> + val actualContent = note["content"] as String + assertEquals("Note does not have correct content after update", updateContent, actualContent) + } + + fun `test searching single note by alert id`() { + client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val noteContent = "test note" + + createAlertNote(alertId, noteContent) + + OpenSearchTestCase.waitUntil({ + return@waitUntil false + }, 3, TimeUnit.SECONDS) + + val search = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).toString() + val searchResponse = client().makeRequest( + "GET", + "$ALERTING_BASE_URI/alerts/notes/_search", + StringEntity(search, ContentType.APPLICATION_JSON) + ) + + val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content) + val hits = xcp.map()["hits"]!! as Map> + logger.info("hits: $hits") + val numberDocsFound = hits["total"]?.get("value") + assertEquals("No Notes found", 1, numberDocsFound) + + val searchHits = hits["hits"] as List<*> + val hit = searchHits[0] as Map<*, *> + val noteHit = hit["_source"] as Map<*, *> + assertEquals("returned Note does not match alert id in search query", alertId, noteHit["entity_id"]) + assertEquals("returned Note does not have expected content", noteContent, noteHit["content"]) + } + + fun `test deleting notes`() { + client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val noteContent = "test note" + + val noteId = createAlertNote(alertId, noteContent).id + OpenSearchTestCase.waitUntil({ + return@waitUntil false + }, 3, TimeUnit.SECONDS) + + val deleteResponse = client().makeRequest( + "DELETE", + "$ALERTING_BASE_URI/alerts/notes/$noteId" + ) + + assertEquals("Delete note failed", RestStatus.OK, deleteResponse.restStatus()) + + val deleteResponseBody = deleteResponse.asMap() + + val deletedNoteId = deleteResponseBody["_id"] as String + assertEquals("Deleted Note ID does not match Note ID in delete request", noteId, deletedNoteId) + } + + // TODO: test list + /* + search notes across multiple alerts + (belongs in NotesIT) create note thats too large based on cluster setting should fail + create note on alert that alrdy has max notes based on cluster setting should fail + create note on alert user doesn't have backend roles to view should fail + search note on alert user doesn't have backend roles to view should fail + notes are shown in notifications for query monitor + notes are shown in notifications for bucket monitor + (belongs in NotesIT) update note that user didn't author should fail + (belongs in NotesIT) delete note that user didn't author should fail + (belongs in NotesIT) update note that user didn't author but user is Admin should succeed + */ } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt index 79c871a97..2510ad8b5 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt @@ -1147,9 +1147,12 @@ class MonitorRestApiIT : AlertingRestTestCase() { assertEquals("Search monitor failed", RestStatus.OK, searchResponse.restStatus()) val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content) val hits = xcp.map()["hits"]!! as Map> + logger.info("hits: $hits") val numberDocsFound = hits["total"]?.get("value") assertEquals("Destination objects are also returned by /_search.", 1, numberDocsFound) + assertEquals("fdsafds", false, true) + val searchHits = hits["hits"] as List val hit = searchHits[0] as Map val monitorHit = hit["_source"] as Map diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt new file mode 100644 index 000000000..e25f6de3e --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt @@ -0,0 +1,3 @@ +package org.opensearch.alerting.resthandler + +class SecureAlertingNotesRestApiIT From 23b4b06123a61d0adaea14b8d2d44e8fda088956 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 29 May 2024 13:37:36 -0700 Subject: [PATCH 06/22] cleanup Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 28 ++++---- .../alerting/BucketLevelMonitorRunner.kt | 12 ++-- .../alerting/alerts/AlertIndices.kt | 12 ++-- .../opensearch/alerting/notes/NotesIndices.kt | 5 +- .../resthandler/RestDeleteNoteAction.kt | 28 ++++---- .../resthandler/RestIndexNoteAction.kt | 65 ++++++++----------- .../resthandler/RestSearchNoteAction.kt | 23 +------ .../transport/TransportDeleteNoteAction.kt | 14 ++-- .../TransportDocLevelMonitorFanOutAction.kt | 3 - .../transport/TransportIndexNoteAction.kt | 9 --- .../opensearch/alerting/util/NotesUtils.kt | 5 ++ .../alerting/alerts/NotesIndicesIT.kt | 10 +-- .../resthandler/AlertingNotesRestApiIT.kt | 8 +++ .../SecureAlertingNotesRestApiIT.kt | 9 ++- 14 files changed, 104 insertions(+), 127 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index 58d6cbdea..acbf32ef0 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -476,21 +476,6 @@ class AlertService( } ?: listOf() } - /** - * Performs a Search request to retrieve the Notes associated with the - * Alert with the given ID. - * - * Searches for the n most recently created Notes based on maxNotes - */ - suspend fun getNotesForAlertNotification(alertId: String, maxNotes: Int): List { - val allNotes = NotesUtils.getNotesByAlertIDs(client, listOf(alertId)) - val sortedNotes = allNotes.sortedByDescending { it.createdTime } - if (sortedNotes.size <= maxNotes) { - return sortedNotes - } - return sortedNotes.slice(0 until maxNotes) - } - suspend fun upsertMonitorErrorAlert( monitor: Monitor, errorMessage: String, @@ -929,4 +914,17 @@ class AlertService( else -> throw IllegalStateException("Unreachable code reached!") } } + + /** + * Performs a Search request to retrieve the top maxNotes most recent Notes associated with the + * given Alert, where maxNotes is a cluster setting. + */ + suspend fun getNotesForAlertNotification(alertId: String, maxNotes: Int): List { + val allNotes = NotesUtils.getNotesByAlertIDs(client, listOf(alertId)) + val sortedNotes = allNotes.sortedByDescending { it.createdTime } + if (sortedNotes.size <= maxNotes) { + return sortedNotes + } + return sortedNotes.slice(0 until maxNotes) + } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index 44828f3a9..6bcb817b4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -298,9 +298,9 @@ object BucketLevelMonitorRunner : MonitorRunner() { for (alert in alertsToExecuteActionsFor) { val alertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(alert.id, maxNotes) val alertContext = if (alertCategory != AlertCategory.NEW) { - AlertContext(alert = alert, notes = alertNotes) + AlertContext(alert = alert, notes = alertNotes.ifEmpty { null }) } else { - getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, alertNotes) + getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, alertNotes.ifEmpty { null }) } val actionCtx = getActionContextForAlertCategory( @@ -336,15 +336,15 @@ object BucketLevelMonitorRunner : MonitorRunner() { val actionCtx = triggerCtx.copy( dedupedAlerts = dedupedAlerts.map { val dedupedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) - AlertContext(alert = it, notes = dedupedAlertsNotes) + AlertContext(alert = it, notes = dedupedAlertsNotes.ifEmpty { null }) }, newAlerts = newAlerts.map { val newAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) - getAlertContext(alert = it, alertSampleDocs = alertSampleDocs, alertNotes = newAlertsNotes) + getAlertContext(alert = it, alertSampleDocs = alertSampleDocs, alertNotes = newAlertsNotes.ifEmpty { null }) }, completedAlerts = completedAlerts.map { val completedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) - AlertContext(alert = it, notes = completedAlertsNotes) + AlertContext(alert = it, notes = completedAlertsNotes.ifEmpty { null }) }, error = monitorResult.error ?: triggerResult.error ) @@ -557,7 +557,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { private fun getAlertContext( alert: Alert, alertSampleDocs: Map>>>, - alertNotes: List + alertNotes: List? ): AlertContext { val bucketKey = alert.aggregationResultBucket?.getBucketKeysHash() val sampleDocs = alertSampleDocs[alert.triggerId]?.get(bucketKey) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index 70b81d4e4..ce5592e34 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -202,7 +202,8 @@ class AlertIndices( } catch (e: Exception) { // This should be run on cluster startup logger.error( - "Error creating alert/finding indices. Alerts/Findings can't be recorded until master node is restarted.", + "Error creating alert/finding indices. " + + "Alerts/Findings can't be recorded until master node is restarted.", e ) } @@ -397,9 +398,7 @@ class AlertIndices( } // TODO call getMapping and compare actual mappings here instead of this - if (targetIndex == IndexUtils.lastUpdatedAlertHistoryIndex || - targetIndex == IndexUtils.lastUpdatedFindingHistoryIndex - ) { + if (targetIndex == IndexUtils.lastUpdatedAlertHistoryIndex || targetIndex == IndexUtils.lastUpdatedFindingHistoryIndex) { return } @@ -494,6 +493,7 @@ class AlertIndices( } private fun deleteOldIndices(tag: String, indices: String) { + logger.info("info deleteOldIndices") val clusterStateRequest = ClusterStateRequest() .clear() .indices(indices) @@ -572,8 +572,7 @@ class AlertIndices( override fun onResponse(deleteIndicesResponse: AcknowledgedResponse) { if (!deleteIndicesResponse.isAcknowledged) { logger.error( - "Could not delete one or more Alerting/Finding history indices: $indicesToDelete." + - "Retrying one by one." + "Could not delete one or more Alerting/Finding history indices: $indicesToDelete. Retrying one by one." ) deleteOldHistoryIndex(indicesToDelete) } @@ -621,7 +620,6 @@ class AlertIndices( val searchSourceBuilder = SearchSourceBuilder() .query(queryBuilder) .version(true) - .seqNoAndPrimaryTerm(true) val searchRequest = SearchRequest() .indices(indexName) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt index 9c690aa63..a5fd5b373 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt @@ -198,6 +198,8 @@ class NotesIndices( // TODO: Everything below here are util functions straight from AlertIndices.kt // TODO: might need to reuse their code or refactor + // TODO: may merge into AlertIndices.kt if we decide to make notes indices + // TODO: component-specific instead of universal and component-agnostic private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { val indicesToDelete = mutableListOf() @@ -300,7 +302,7 @@ class NotesIndices( if (ExceptionsHelper.unwrapCause(t) is ResourceAlreadyExistsException) { true } else { - throw t // TODO: wrap in some kind of NotesException? + throw t } } } @@ -312,7 +314,6 @@ class NotesIndices( targetIndex = IndexUtils.getIndexNameWithAlias(clusterState, index) } - // TODO call getMapping and compare actual mappings here instead of this if (targetIndex == IndexUtils.lastUpdatedNotesHistoryIndex ) { return diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt index fab9559a9..a38402a63 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.alerting.resthandler import org.apache.logging.log4j.LogManager @@ -7,15 +12,16 @@ import org.opensearch.client.node.NodeClient import org.opensearch.commons.alerting.action.AlertingActions import org.opensearch.commons.alerting.action.DeleteNoteRequest import org.opensearch.rest.BaseRestHandler -import org.opensearch.rest.RestHandler.ReplacedRoute import org.opensearch.rest.RestHandler.Route import org.opensearch.rest.RestRequest -import org.opensearch.rest.RestRequest.Method.DELETE import org.opensearch.rest.action.RestToXContentListener import java.io.IOException private val log: Logger = LogManager.getLogger(RestDeleteMonitorAction::class.java) +/** + * Rest handlers to create and update notes. + */ class RestDeleteNoteAction : BaseRestHandler() { override fun getName(): String { @@ -23,16 +29,10 @@ class RestDeleteNoteAction : BaseRestHandler() { } override fun routes(): List { - return listOf() - } - - override fun replacedRoutes(): MutableList { - return mutableListOf( - ReplacedRoute( - DELETE, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}", - DELETE, - "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/notes/{noteID}" + return listOf( + Route( + RestRequest.Method.DELETE, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}" ) ) } @@ -40,12 +40,8 @@ class RestDeleteNoteAction : BaseRestHandler() { @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}") - val noteId = request.param("noteID") - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/$noteId") - val deleteMonitorRequest = DeleteNoteRequest(noteId) - return RestChannelConsumer { channel -> client.execute(AlertingActions.DELETE_NOTES_ACTION_TYPE, deleteMonitorRequest, RestToXContentListener(channel)) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt index 11b297770..1d4b996b8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt @@ -22,11 +22,8 @@ import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.rest.BaseRestHandler import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestChannel -import org.opensearch.rest.RestHandler.ReplacedRoute import org.opensearch.rest.RestHandler.Route import org.opensearch.rest.RestRequest -import org.opensearch.rest.RestRequest.Method.POST -import org.opensearch.rest.RestRequest.Method.PUT import org.opensearch.rest.RestResponse import org.opensearch.rest.action.RestResponseListener import java.io.IOException @@ -43,22 +40,14 @@ class RestIndexNoteAction : BaseRestHandler() { } override fun routes(): List { - return listOf() - } - - override fun replacedRoutes(): MutableList { - return mutableListOf( - ReplacedRoute( - POST, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/{alertID}/notes", - POST, - "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/{alertID}/notes", + return listOf( + Route( + RestRequest.Method.POST, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/{alertID}/notes" ), - ReplacedRoute( - PUT, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}", - PUT, - "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/notes/{noteID}", + Route( + RestRequest.Method.PUT, + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}" ) ) } @@ -69,15 +58,16 @@ class RestIndexNoteAction : BaseRestHandler() { val alertId = request.param("alertID", Alert.NO_ID) val noteId = request.param("noteID", Note.NO_ID) - if (request.method() == POST && Alert.NO_ID == alertId) { + if (request.method() == RestRequest.Method.POST && Alert.NO_ID == alertId) { throw AlertingException.wrap(IllegalArgumentException("Missing alert ID")) - } else if (request.method() == PUT && Note.NO_ID == noteId) { + } else if (request.method() == RestRequest.Method.PUT && Note.NO_ID == noteId) { throw AlertingException.wrap(IllegalArgumentException("Missing note ID")) } - // TODO: validation for empty string? val content = request.contentParser().map()["content"] as String? - ?: throw AlertingException.wrap(IllegalArgumentException("Missing note content")) + if (content.isNullOrEmpty()) { + throw AlertingException.wrap(IllegalArgumentException("Missing note content")) + } val seqNo = request.paramAsLong(IF_SEQ_NO, SequenceNumbers.UNASSIGNED_SEQ_NO) val primaryTerm = request.paramAsLong(IF_PRIMARY_TERM, SequenceNumbers.UNASSIGNED_PRIMARY_TERM) @@ -87,22 +77,23 @@ class RestIndexNoteAction : BaseRestHandler() { client.execute(AlertingActions.INDEX_NOTE_ACTION_TYPE, indexNoteRequest, indexNoteResponse(channel, request.method())) } } -} -private fun indexNoteResponse(channel: RestChannel, restMethod: RestRequest.Method): - RestResponseListener { - return object : RestResponseListener(channel) { - @Throws(Exception::class) - override fun buildResponse(response: IndexNoteResponse): RestResponse { - var returnStatus = RestStatus.CREATED - if (restMethod == RestRequest.Method.PUT) - returnStatus = RestStatus.OK - val restResponse = BytesRestResponse(returnStatus, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) -// if (returnStatus == RestStatus.CREATED) { -// val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/${response.id}" -// restResponse.addHeader("Location", location) -// } - return restResponse + private fun indexNoteResponse(channel: RestChannel, restMethod: RestRequest.Method): + RestResponseListener { + return object : RestResponseListener(channel) { + @Throws(Exception::class) + override fun buildResponse(response: IndexNoteResponse): RestResponse { + var returnStatus = RestStatus.CREATED + if (restMethod == RestRequest.Method.PUT) + returnStatus = RestStatus.OK + + val restResponse = BytesRestResponse(returnStatus, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) + if (returnStatus == RestStatus.CREATED) { + val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/${response.id}" + restResponse.addHeader("Location", location) + } + return restResponse + } } } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt index 2f9a6af47..eb6ce9342 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt @@ -24,7 +24,6 @@ import org.opensearch.core.xcontent.ToXContent.EMPTY_PARAMS import org.opensearch.rest.BaseRestHandler import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestChannel -import org.opensearch.rest.RestHandler.ReplacedRoute import org.opensearch.rest.RestHandler.Route import org.opensearch.rest.RestRequest import org.opensearch.rest.RestResponse @@ -44,16 +43,10 @@ class RestSearchNoteAction() : BaseRestHandler() { } override fun routes(): List { - return listOf() - } - - override fun replacedRoutes(): MutableList { - return mutableListOf( - ReplacedRoute( - RestRequest.Method.GET, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/_search", + return listOf( + Route( RestRequest.Method.GET, - "${AlertingPlugin.LEGACY_OPENDISTRO_MONITOR_BASE_URI}/alerts/notes/_search", + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/_search" ) ) } @@ -92,18 +85,8 @@ class RestSearchNoteAction() : BaseRestHandler() { LoggingDeprecationHandler.INSTANCE, hit.sourceAsString ).use { hitsParser -> - log.info("hit sourceAsString: ${hit.sourceAsString}") - // at parser's initialization, it points at null, - // need to call nextToken() to get it to point to - // the beginning of the hit object - // TODO: bring this up with team, currently SearchMonitorRestHandler - // doesn't do this, causing the parse to fail, causing search response - // to show objects as is, which bypasses Monitor's toXContent which - // doesn't show User, meaning the Search response shows the whole User - // object, which would be exposure of sensitive information hitsParser.nextToken() val note = Note.parse(hitsParser, hit.id) - log.info("note: $note") val xcb = note.toXContent(jsonBuilder(), EMPTY_PARAMS) hit.sourceRef(BytesReference.bytes(xcb)) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt index 35345a678..f947ec857 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.alerting.transport import kotlinx.coroutines.CoroutineScope @@ -7,7 +12,6 @@ import org.apache.logging.log4j.LogManager import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionRequest import org.opensearch.action.delete.DeleteRequest -import org.opensearch.action.delete.DeleteResponse import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters @@ -120,7 +124,8 @@ class TransportDeleteNoteAction @Inject constructor( val deleteRequest = DeleteRequest(sourceIndex, noteId) if (canDelete) { - val deleteResponse = deleteNote(deleteRequest) + log.debug("Deleting the note with id ${deleteRequest.id()}") + val deleteResponse = client.suspendUntil { delete(deleteRequest, it) } actionListener.onResponse(DeleteNoteResponse(deleteResponse.id)) } else { actionListener.onFailure( @@ -170,9 +175,4 @@ class TransportDeleteNoteAction @Inject constructor( return notes[0] // we searched on Note ID, there should only be one Note in the List } } - - private suspend fun deleteNote(deleteRequest: DeleteRequest): DeleteResponse { - log.debug("Deleting the note with id ${deleteRequest.id()}") - return client.suspendUntil { delete(deleteRequest, it) } - } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt index 6e902c676..f1cf12831 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt @@ -1051,9 +1051,6 @@ class TransportDocLevelMonitorFanOutAction val docFieldTags = parseSampleDocTags(monitor.triggers) val request = MultiGetRequest() - // TODO: model after this code, notifs r only resent for active alerts, which means - // TODO: ull only ever need to query the one index of active notes, which means u hav a - // TODO: concrete, which means u can copy this and use mget // Perform mGet request in batches. findingToDocPairs.chunked(findingsIndexBatchSize).forEach { batch -> batch.forEach { (findingId, docIdAndIndex) -> diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt index ffd213a2f..799193bf6 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt @@ -287,15 +287,6 @@ constructor( // retains everything from the original note except content and lastUpdatedTime val requestNote = currentNote.copy(content = request.content, lastUpdatedTime = Instant.now()) -// val requestNote = -// Note( -// id = currentNote.id, -// alertId = currentNote.alertId, -// content = request.content, -// time = Instant.now(), -// user = currentNote.user, -// ) - val indexRequest = IndexRequest(NOTES_HISTORY_WRITE_INDEX) .source(requestNote.toXContentWithUser(XContentFactory.jsonBuilder())) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt index 400001f98..cefccb3b1 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.alerting.util import org.apache.logging.log4j.LogManager diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt index b882d412e..dd04bf3f3 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/alerts/NotesIndicesIT.kt @@ -1,8 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.alerting.alerts import org.opensearch.alerting.AlertingRestTestCase -class NotesIndicesIT : AlertingRestTestCase() { - fun `test create initial notes index`() { - } -} +class NotesIndicesIT : AlertingRestTestCase() diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt index 2b306211c..eb8ffc00d 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.alerting.resthandler import org.apache.hc.core5.http.ContentType @@ -131,6 +136,9 @@ class AlertingNotesRestApiIT : AlertingRestTestCase() { // TODO: test list /* + create note with empty content should fail + create without alert id should fail + update without note id should fail search notes across multiple alerts (belongs in NotesIT) create note thats too large based on cluster setting should fail create note on alert that alrdy has max notes based on cluster setting should fail diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt index e25f6de3e..a16f92d3b 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureAlertingNotesRestApiIT.kt @@ -1,3 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.alerting.resthandler -class SecureAlertingNotesRestApiIT +import org.opensearch.alerting.AlertingRestTestCase + +class SecureAlertingNotesRestApiIT : AlertingRestTestCase() From 2f4752bf797c47890d3c111038b90ec0213898d0 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Wed, 5 Jun 2024 09:00:14 -0700 Subject: [PATCH 07/22] renaming notes to comments Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 30 ++-- .../org/opensearch/alerting/AlertingPlugin.kt | 50 +++--- .../alerting/BucketLevelMonitorRunner.kt | 34 ++-- .../alerting/QueryLevelMonitorRunner.kt | 6 +- .../alerting/alerts/AlertIndices.kt | 10 +- .../CommentsIndices.kt} | 146 ++++++++--------- .../opensearch/alerting/model/AlertContext.kt | 18 +-- ....kt => RestDeleteAlertingCommentAction.kt} | 18 +-- ...n.kt => RestIndexAlertingCommentAction.kt} | 40 ++--- ....kt => RestSearchAlertingCommentAction.kt} | 30 ++-- .../alerting/settings/AlertingSettings.kt | 36 ++--- ...> TransportDeleteAlertingCommentAction.kt} | 78 ++++----- ...=> TransportIndexAlertingCommentAction.kt} | 142 ++++++++-------- ...> TransportSearchAlertingCommentAction.kt} | 40 ++--- .../util/{NotesUtils.kt => CommentsUtils.kt} | 44 ++--- .../opensearch/alerting/util/IndexUtils.kt | 14 +- .../alerting_comments.json} | 0 .../alerting/AlertingRestTestCase.kt | 34 ++-- .../resthandler/AlertingCommentsRestApiIT.kt | 153 ++++++++++++++++++ .../resthandler/AlertingNotesRestApiIT.kt | 153 ------------------ 20 files changed, 544 insertions(+), 532 deletions(-) rename alerting/src/main/kotlin/org/opensearch/alerting/{notes/NotesIndices.kt => comments/CommentsIndices.kt} (71%) rename alerting/src/main/kotlin/org/opensearch/alerting/resthandler/{RestDeleteNoteAction.kt => RestDeleteAlertingCommentAction.kt} (65%) rename alerting/src/main/kotlin/org/opensearch/alerting/resthandler/{RestIndexNoteAction.kt => RestIndexAlertingCommentAction.kt} (71%) rename alerting/src/main/kotlin/org/opensearch/alerting/resthandler/{RestSearchNoteAction.kt => RestSearchAlertingCommentAction.kt} (74%) rename alerting/src/main/kotlin/org/opensearch/alerting/transport/{TransportDeleteNoteAction.kt => TransportDeleteAlertingCommentAction.kt} (65%) rename alerting/src/main/kotlin/org/opensearch/alerting/transport/{TransportIndexNoteAction.kt => TransportIndexAlertingCommentAction.kt} (69%) rename alerting/src/main/kotlin/org/opensearch/alerting/transport/{TransportSearchNoteAction.kt => TransportSearchAlertingCommentAction.kt} (80%) rename alerting/src/main/kotlin/org/opensearch/alerting/util/{NotesUtils.kt => CommentsUtils.kt} (66%) rename alerting/src/main/resources/org/opensearch/alerting/{notes/alerting_notes.json => comments/alerting_comments.json} (100%) create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt delete mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index acbf32ef0..fe0c1e58d 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -28,9 +28,9 @@ import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.script.ChainedAlertTriggerExecutionContext import org.opensearch.alerting.script.DocumentLevelTriggerExecutionContext import org.opensearch.alerting.script.QueryLevelTriggerExecutionContext +import org.opensearch.alerting.util.CommentsUtils import org.opensearch.alerting.util.IndexUtils import org.opensearch.alerting.util.MAX_SEARCH_SIZE -import org.opensearch.alerting.util.NotesUtils import org.opensearch.alerting.util.getBucketKeysHash import org.opensearch.alerting.workflow.WorkflowRunContext import org.opensearch.client.Client @@ -44,10 +44,10 @@ import org.opensearch.commons.alerting.model.ActionExecutionResult import org.opensearch.commons.alerting.model.AggregationResultBucket import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.BucketLevelTrigger +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.alerting.model.DataSources import org.opensearch.commons.alerting.model.Monitor import org.opensearch.commons.alerting.model.NoOpTrigger -import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.Trigger import org.opensearch.commons.alerting.model.Workflow import org.opensearch.commons.alerting.model.action.AlertCategory @@ -686,7 +686,7 @@ class AlertService( val alertsIndex = dataSources.alertsIndex val alertsHistoryIndex = dataSources.alertsHistoryIndex - val notesToDeleteIDs = mutableListOf() + val commentsToDeleteIDs = mutableListOf() var requestsToRetry = alerts.flatMap { alert -> // We don't want to set the version when saving alerts because the MonitorRunner has first priority when writing alerts. @@ -741,9 +741,9 @@ class AlertService( .source(alert.toXContentWithUser(XContentFactory.jsonBuilder())) .id(alert.id) } else { - // Otherwise, prepare the Alert's Notes for deletion, and don't include + // Otherwise, prepare the Alert's comments for deletion, and don't include // a request to index the Alert to an Alert history index - notesToDeleteIDs.addAll(NotesUtils.getNoteIDsByAlertIDs(client, listOf(alert.id))) + commentsToDeleteIDs.addAll(CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id))) null } ) @@ -766,8 +766,8 @@ class AlertService( } } - // delete all the Notes of any Alerts that were deleted - NotesUtils.deleteNotes(client, notesToDeleteIDs) + // delete all the comments of any Alerts that were deleted + CommentsUtils.deleteComments(client, commentsToDeleteIDs) } /** @@ -916,15 +916,15 @@ class AlertService( } /** - * Performs a Search request to retrieve the top maxNotes most recent Notes associated with the - * given Alert, where maxNotes is a cluster setting. + * Performs a Search request to retrieve the top maxComments most recent comments associated with the + * given Alert, where maxComments is a cluster setting. */ - suspend fun getNotesForAlertNotification(alertId: String, maxNotes: Int): List { - val allNotes = NotesUtils.getNotesByAlertIDs(client, listOf(alertId)) - val sortedNotes = allNotes.sortedByDescending { it.createdTime } - if (sortedNotes.size <= maxNotes) { - return sortedNotes + suspend fun getCommentsForAlertNotification(alertId: String, maxComments: Int): List { + val allcomments = CommentsUtils.getCommentsByAlertIDs(client, listOf(alertId)) + val sortedcomments = allcomments.sortedByDescending { it.createdTime } + if (sortedcomments.size <= maxComments) { + return sortedcomments } - return sortedNotes.slice(0 until maxNotes) + return sortedcomments.slice(0 until maxComments) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index d960f1fe8..e32fc7cd7 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -16,6 +16,7 @@ import org.opensearch.alerting.action.GetRemoteIndexesAction import org.opensearch.alerting.action.SearchEmailAccountAction import org.opensearch.alerting.action.SearchEmailGroupAction import org.opensearch.alerting.alerts.AlertIndices +import org.opensearch.alerting.comments.CommentsIndices import org.opensearch.alerting.core.JobSweeper import org.opensearch.alerting.core.ScheduledJobIndices import org.opensearch.alerting.core.action.node.ScheduledJobsStatsAction @@ -25,11 +26,10 @@ import org.opensearch.alerting.core.resthandler.RestScheduledJobStatsHandler import org.opensearch.alerting.core.schedule.JobScheduler import org.opensearch.alerting.core.settings.LegacyOpenDistroScheduledJobSettings import org.opensearch.alerting.core.settings.ScheduledJobSettings -import org.opensearch.alerting.notes.NotesIndices import org.opensearch.alerting.resthandler.RestAcknowledgeAlertAction import org.opensearch.alerting.resthandler.RestAcknowledgeChainedAlertAction +import org.opensearch.alerting.resthandler.RestDeleteAlertingCommentAction import org.opensearch.alerting.resthandler.RestDeleteMonitorAction -import org.opensearch.alerting.resthandler.RestDeleteNoteAction import org.opensearch.alerting.resthandler.RestDeleteWorkflowAction import org.opensearch.alerting.resthandler.RestExecuteMonitorAction import org.opensearch.alerting.resthandler.RestExecuteWorkflowAction @@ -42,13 +42,13 @@ import org.opensearch.alerting.resthandler.RestGetMonitorAction import org.opensearch.alerting.resthandler.RestGetRemoteIndexesAction import org.opensearch.alerting.resthandler.RestGetWorkflowAction import org.opensearch.alerting.resthandler.RestGetWorkflowAlertsAction +import org.opensearch.alerting.resthandler.RestIndexAlertingCommentAction import org.opensearch.alerting.resthandler.RestIndexMonitorAction -import org.opensearch.alerting.resthandler.RestIndexNoteAction import org.opensearch.alerting.resthandler.RestIndexWorkflowAction +import org.opensearch.alerting.resthandler.RestSearchAlertingCommentAction import org.opensearch.alerting.resthandler.RestSearchEmailAccountAction import org.opensearch.alerting.resthandler.RestSearchEmailGroupAction import org.opensearch.alerting.resthandler.RestSearchMonitorAction -import org.opensearch.alerting.resthandler.RestSearchNoteAction import org.opensearch.alerting.script.TriggerScript import org.opensearch.alerting.service.DeleteMonitorService import org.opensearch.alerting.settings.AlertingSettings @@ -58,8 +58,8 @@ import org.opensearch.alerting.settings.LegacyOpenDistroAlertingSettings import org.opensearch.alerting.settings.LegacyOpenDistroDestinationSettings import org.opensearch.alerting.transport.TransportAcknowledgeAlertAction import org.opensearch.alerting.transport.TransportAcknowledgeChainedAlertAction +import org.opensearch.alerting.transport.TransportDeleteAlertingCommentAction import org.opensearch.alerting.transport.TransportDeleteMonitorAction -import org.opensearch.alerting.transport.TransportDeleteNoteAction import org.opensearch.alerting.transport.TransportDeleteWorkflowAction import org.opensearch.alerting.transport.TransportDocLevelMonitorFanOutAction import org.opensearch.alerting.transport.TransportExecuteMonitorAction @@ -73,13 +73,13 @@ import org.opensearch.alerting.transport.TransportGetMonitorAction import org.opensearch.alerting.transport.TransportGetRemoteIndexesAction import org.opensearch.alerting.transport.TransportGetWorkflowAction import org.opensearch.alerting.transport.TransportGetWorkflowAlertsAction +import org.opensearch.alerting.transport.TransportIndexAlertingCommentAction import org.opensearch.alerting.transport.TransportIndexMonitorAction -import org.opensearch.alerting.transport.TransportIndexNoteAction import org.opensearch.alerting.transport.TransportIndexWorkflowAction +import org.opensearch.alerting.transport.TransportSearchAlertingCommentAction import org.opensearch.alerting.transport.TransportSearchEmailAccountAction import org.opensearch.alerting.transport.TransportSearchEmailGroupAction import org.opensearch.alerting.transport.TransportSearchMonitorAction -import org.opensearch.alerting.transport.TransportSearchNoteAction import org.opensearch.alerting.util.DocLevelMonitorQueries import org.opensearch.alerting.util.destinationmigration.DestinationMigrationCoordinator import org.opensearch.client.Client @@ -164,7 +164,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R lateinit var scheduler: JobScheduler lateinit var sweeper: JobSweeper lateinit var scheduledJobIndices: ScheduledJobIndices - lateinit var notesIndices: NotesIndices + lateinit var commentsIndices: CommentsIndices lateinit var docLevelMonitorQueries: DocLevelMonitorQueries lateinit var threadPool: ThreadPool lateinit var alertIndices: AlertIndices @@ -202,9 +202,9 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R RestGetWorkflowAction(), RestDeleteWorkflowAction(), RestGetRemoteIndexesAction(), - RestIndexNoteAction(), - RestSearchNoteAction(), - RestDeleteNoteAction(), + RestIndexAlertingCommentAction(), + RestSearchAlertingCommentAction(), + RestDeleteAlertingCommentAction(), ) } @@ -231,9 +231,9 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R ActionPlugin.ActionHandler(AlertingActions.INDEX_WORKFLOW_ACTION_TYPE, TransportIndexWorkflowAction::class.java), ActionPlugin.ActionHandler(AlertingActions.GET_WORKFLOW_ACTION_TYPE, TransportGetWorkflowAction::class.java), ActionPlugin.ActionHandler(AlertingActions.DELETE_WORKFLOW_ACTION_TYPE, TransportDeleteWorkflowAction::class.java), - ActionPlugin.ActionHandler(AlertingActions.INDEX_NOTE_ACTION_TYPE, TransportIndexNoteAction::class.java), - ActionPlugin.ActionHandler(AlertingActions.SEARCH_NOTES_ACTION_TYPE, TransportSearchNoteAction::class.java), - ActionPlugin.ActionHandler(AlertingActions.DELETE_NOTES_ACTION_TYPE, TransportDeleteNoteAction::class.java), + ActionPlugin.ActionHandler(AlertingActions.INDEX_COMMENT_ACTION_TYPE, TransportIndexAlertingCommentAction::class.java), + ActionPlugin.ActionHandler(AlertingActions.SEARCH_COMMENTS_ACTION_TYPE, TransportSearchAlertingCommentAction::class.java), + ActionPlugin.ActionHandler(AlertingActions.DELETE_COMMENT_ACTION_TYPE, TransportDeleteAlertingCommentAction::class.java), ActionPlugin.ActionHandler(ExecuteWorkflowAction.INSTANCE, TransportExecuteWorkflowAction::class.java), ActionPlugin.ActionHandler(GetRemoteIndexesAction.INSTANCE, TransportGetRemoteIndexesAction::class.java), ActionPlugin.ActionHandler(DocLevelMonitorFanOutAction.INSTANCE, TransportDocLevelMonitorFanOutAction::class.java) @@ -292,7 +292,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R .registerConsumers() .registerDestinationSettings() scheduledJobIndices = ScheduledJobIndices(client.admin(), clusterService) - notesIndices = NotesIndices(environment.settings(), client, threadPool, clusterService) + commentsIndices = CommentsIndices(environment.settings(), client, threadPool, clusterService) docLevelMonitorQueries = DocLevelMonitorQueries(client, clusterService) scheduler = JobScheduler(threadPool, runner) sweeper = JobSweeper(environment.settings(), client, clusterService, threadPool, xContentRegistry, scheduler, ALERTING_JOB_TYPES) @@ -321,7 +321,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R scheduler, runner, scheduledJobIndices, - notesIndices, + commentsIndices, docLevelMonitorQueries, destinationMigrationCoordinator, lockService, @@ -397,15 +397,15 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.FINDING_HISTORY_RETENTION_PERIOD, AlertingSettings.FINDINGS_INDEXING_BATCH_SIZE, AlertingSettings.CROSS_CLUSTER_MONITORING_ENABLED, - AlertingSettings.ALERTING_NOTES_ENABLED, - AlertingSettings.NOTES_HISTORY_ENABLED, - AlertingSettings.NOTES_HISTORY_MAX_DOCS, - AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE, - AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD, - AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD, - AlertingSettings.NOTES_MAX_CONTENT_SIZE, - AlertingSettings.MAX_NOTES_PER_ALERT, - AlertingSettings.MAX_NOTES_PER_NOTIFICATION + AlertingSettings.ALERTING_COMMENTS_ENABLED, + AlertingSettings.COMMENTS_HISTORY_ENABLED, + AlertingSettings.COMMENTS_HISTORY_MAX_DOCS, + AlertingSettings.COMMENTS_HISTORY_INDEX_MAX_AGE, + AlertingSettings.COMMENTS_HISTORY_ROLLOVER_PERIOD, + AlertingSettings.COMMENTS_HISTORY_RETENTION_PERIOD, + AlertingSettings.COMMENTS_MAX_CONTENT_SIZE, + AlertingSettings.MAX_COMMENTS_PER_ALERT, + AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index 6bcb817b4..7f991d096 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -37,9 +37,9 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.XContentType import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.BucketLevelTrigger +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.alerting.model.Finding import org.opensearch.commons.alerting.model.Monitor -import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.SearchInput import org.opensearch.commons.alerting.model.action.AlertCategory import org.opensearch.commons.alerting.model.action.PerAlertActionScope @@ -194,7 +194,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { * The new Alerts have to be returned and saved back with their indexed doc ID to prevent duplicate documents * when the Alerts are updated again after Action execution. * - * Note: Index operations can fail for various reasons (such as write blocks on cluster), in such a case, the Actions + * Comment: Index operations can fail for various reasons (such as write blocks on cluster), in such a case, the Actions * will still execute with the Alert information in the ctx but the Alerts may not be visible. */ if (!dryrun && monitor.id != Monitor.NO_ID) { @@ -275,8 +275,8 @@ object BucketLevelMonitorRunner : MonitorRunner() { // to alertsToUpdate to ensure the Alert doc is updated at the end in either case completedAlertsToUpdate.addAll(completedAlerts) - // retrieve max Notes per Alert notification setting - val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) + // retrieve max Comments per Alert notification setting + val maxComments = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION) // All trigger contexts and results should be available at this point since all triggers were evaluated in the main do-while loop val triggerCtx = triggerContexts[trigger.id]!! @@ -296,11 +296,11 @@ object BucketLevelMonitorRunner : MonitorRunner() { for (alertCategory in actionExecutionScope.actionableAlerts) { val alertsToExecuteActionsFor = nextAlerts[trigger.id]?.get(alertCategory) ?: mutableListOf() for (alert in alertsToExecuteActionsFor) { - val alertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(alert.id, maxNotes) + val alertComments = monitorCtx.alertService!!.getCommentsForAlertNotification(alert.id, maxComments) val alertContext = if (alertCategory != AlertCategory.NEW) { - AlertContext(alert = alert, notes = alertNotes.ifEmpty { null }) + AlertContext(alert = alert, comments = alertComments.ifEmpty { null }) } else { - getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, alertNotes.ifEmpty { null }) + getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, alertComments.ifEmpty { null }) } val actionCtx = getActionContextForAlertCategory( @@ -335,16 +335,20 @@ object BucketLevelMonitorRunner : MonitorRunner() { val actionCtx = triggerCtx.copy( dedupedAlerts = dedupedAlerts.map { - val dedupedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) - AlertContext(alert = it, notes = dedupedAlertsNotes.ifEmpty { null }) + val dedupedAlertsComments = monitorCtx.alertService!!.getCommentsForAlertNotification(it.id, maxComments) + AlertContext(alert = it, comments = dedupedAlertsComments.ifEmpty { null }) }, newAlerts = newAlerts.map { - val newAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) - getAlertContext(alert = it, alertSampleDocs = alertSampleDocs, alertNotes = newAlertsNotes.ifEmpty { null }) + val newAlertsComments = monitorCtx.alertService!!.getCommentsForAlertNotification(it.id, maxComments) + getAlertContext( + alert = it, + alertSampleDocs = alertSampleDocs, + alertComments = newAlertsComments.ifEmpty { null } + ) }, completedAlerts = completedAlerts.map { - val completedAlertsNotes = monitorCtx.alertService!!.getNotesForAlertNotification(it.id, maxNotes) - AlertContext(alert = it, notes = completedAlertsNotes.ifEmpty { null }) + val completedAlertsComments = monitorCtx.alertService!!.getCommentsForAlertNotification(it.id, maxComments) + AlertContext(alert = it, comments = completedAlertsComments.ifEmpty { null }) }, error = monitorResult.error ?: triggerResult.error ) @@ -557,7 +561,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { private fun getAlertContext( alert: Alert, alertSampleDocs: Map>>>, - alertNotes: List? + alertComments: List? ): AlertContext { val bucketKey = alert.aggregationResultBucket?.getBucketKeysHash() val sampleDocs = alertSampleDocs[alert.triggerId]?.get(bucketKey) @@ -571,7 +575,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { alert.monitorId, alert.executionId ) - AlertContext(alert = alert, sampleDocs = listOf(), notes = alertNotes) + AlertContext(alert = alert, sampleDocs = listOf(), comments = alertComments) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index 0a8be3ace..f295f5dc0 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -69,9 +69,9 @@ object QueryLevelMonitorRunner : MonitorRunner() { for (trigger in monitor.triggers) { val currentAlert = currentAlerts[trigger] val currentAlertContext = currentAlert?.let { - val maxNotes = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_NOTES_PER_NOTIFICATION) - val currentAlertNotes = monitorCtx.alertService!!.getNotesForAlertNotification(currentAlert.id, maxNotes) - AlertContext(alert = currentAlert, notes = currentAlertNotes.ifEmpty { null }) + val maxComments = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION) + val currentAlertComments = monitorCtx.alertService!!.getCommentsForAlertNotification(currentAlert.id, maxComments) + AlertContext(alert = currentAlert, comments = currentAlertComments.ifEmpty { null }) } val triggerCtx = QueryLevelTriggerExecutionContext(monitor, trigger as QueryLevelTrigger, monitorResult, currentAlertContext) val triggerResult = when (monitor.monitorType) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index ce5592e34..78630ae3c 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -42,8 +42,8 @@ import org.opensearch.alerting.settings.AlertingSettings.Companion.FINDING_HISTO import org.opensearch.alerting.settings.AlertingSettings.Companion.FINDING_HISTORY_ROLLOVER_PERIOD import org.opensearch.alerting.settings.AlertingSettings.Companion.REQUEST_TIMEOUT import org.opensearch.alerting.util.AlertingException +import org.opensearch.alerting.util.CommentsUtils import org.opensearch.alerting.util.IndexUtils -import org.opensearch.alerting.util.NotesUtils import org.opensearch.client.Client import org.opensearch.cluster.ClusterChangedEvent import org.opensearch.cluster.ClusterStateListener @@ -509,7 +509,7 @@ class AlertIndices( val indicesToDelete = getIndicesToDelete(clusterStateResponse) logger.info("Deleting old $tag indices viz $indicesToDelete") if (indices == ALERT_HISTORY_ALL) { - deleteAlertNotes(indicesToDelete) + deleteAlertComments(indicesToDelete) } deleteAllOldHistoryIndices(indicesToDelete) } @@ -607,11 +607,11 @@ class AlertIndices( } } - private suspend fun deleteAlertNotes(alertHistoryIndicesToDelete: List) { + private suspend fun deleteAlertComments(alertHistoryIndicesToDelete: List) { alertHistoryIndicesToDelete.forEach { alertHistoryIndex -> val alertIDs = getAlertIDsFromAlertHistoryIndex(alertHistoryIndex) - val notesToDeleteIDs = NotesUtils.getNoteIDsByAlertIDs(client, alertIDs) - NotesUtils.deleteNotes(client, notesToDeleteIDs) + val commentsToDeleteIDs = CommentsUtils.getCommentIDsByAlertIDs(client, alertIDs) + CommentsUtils.deleteComments(client, commentsToDeleteIDs) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt similarity index 71% rename from alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt index a5fd5b373..be92e5028 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/notes/NotesIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.opensearch.alerting.notes +package org.opensearch.alerting.comments import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper @@ -39,10 +39,10 @@ import org.opensearch.threadpool.ThreadPool import java.time.Instant /** - * Initialize the OpenSearch components required to run Notes. + * Initialize the OpenSearch components required to run comments. * */ -class NotesIndices( +class CommentsIndices( settings: Settings, private val client: Client, private val threadPool: ThreadPool, @@ -50,59 +50,61 @@ class NotesIndices( ) : ClusterStateListener { init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_ENABLED) { notesHistoryEnabled = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_MAX_DOCS) { notesHistoryMaxDocs = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE) { notesHistoryMaxAge = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD) { - notesHistoryRolloverPeriod = it - rescheduleNotesRollover() + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_ENABLED) { commentsHistoryEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_MAX_DOCS) { commentsHistoryMaxDocs = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_INDEX_MAX_AGE) { + commentsHistoryMaxAge = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD) { - notesHistoryRetentionPeriod = it + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_ROLLOVER_PERIOD) { + commentsHistoryRolloverPeriod = it + rescheduleCommentsRollover() + } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_RETENTION_PERIOD) { + commentsHistoryRetentionPeriod = it } } companion object { - /** The alias of the index in which to write notes finding */ - const val NOTES_HISTORY_WRITE_INDEX = ".opensearch-alerting-notes-history-write" + /** The alias of the index in which to write comments finding */ + const val COMMENTS_HISTORY_WRITE_INDEX = ".opensearch-alerting-comments-history-write" - /** The index name pattern referring to all notes history indices */ - const val NOTES_HISTORY_ALL = ".opensearch-alerting-notes-history*" + /** The index name pattern referring to all comments history indices */ + const val COMMENTS_HISTORY_ALL = ".opensearch-alerting-comments-history*" - /** The index name pattern to create notes history indices */ - const val NOTES_HISTORY_INDEX_PATTERN = "<.opensearch-alerting-notes-history-{now/d}-1>" + /** The index name pattern to create comments history indices */ + const val COMMENTS_HISTORY_INDEX_PATTERN = "<.opensearch-alerting-comments-history-{now/d}-1>" - /** The index name pattern to query all notes, history and current notes. */ - const val ALL_NOTES_INDEX_PATTERN = ".opensearch-alerting-notes*" + /** The index name pattern to query all comments, history and current comments. */ + const val ALL_COMMENTS_INDEX_PATTERN = ".opensearch-alerting-comments*" @JvmStatic - fun notesMapping() = - NotesIndices::class.java.getResource("alerting_notes.json").readText() + fun commentsMapping() = + CommentsIndices::class.java.getResource("alerting_comments.json").readText() private val logger = LogManager.getLogger(AlertIndices::class.java) } - @Volatile private var notesHistoryEnabled = AlertingSettings.NOTES_HISTORY_ENABLED.get(settings) + @Volatile private var commentsHistoryEnabled = AlertingSettings.COMMENTS_HISTORY_ENABLED.get(settings) - @Volatile private var notesHistoryMaxDocs = AlertingSettings.NOTES_HISTORY_MAX_DOCS.get(settings) + @Volatile private var commentsHistoryMaxDocs = AlertingSettings.COMMENTS_HISTORY_MAX_DOCS.get(settings) - @Volatile private var notesHistoryMaxAge = AlertingSettings.NOTES_HISTORY_INDEX_MAX_AGE.get(settings) + @Volatile private var commentsHistoryMaxAge = AlertingSettings.COMMENTS_HISTORY_INDEX_MAX_AGE.get(settings) - @Volatile private var notesHistoryRolloverPeriod = AlertingSettings.NOTES_HISTORY_ROLLOVER_PERIOD.get(settings) + @Volatile private var commentsHistoryRolloverPeriod = AlertingSettings.COMMENTS_HISTORY_ROLLOVER_PERIOD.get(settings) - @Volatile private var notesHistoryRetentionPeriod = AlertingSettings.NOTES_HISTORY_RETENTION_PERIOD.get(settings) + @Volatile private var commentsHistoryRetentionPeriod = AlertingSettings.COMMENTS_HISTORY_RETENTION_PERIOD.get(settings) @Volatile private var isClusterManager = false // for JobsMonitor to report var lastRolloverTime: TimeValue? = null - private var notesHistoryIndexInitialized: Boolean = false + private var commentsHistoryIndexInitialized: Boolean = false - private var scheduledNotesRollover: Scheduler.Cancellable? = null + private var scheduledCommentsRollover: Scheduler.Cancellable? = null /** - * Initialize the indices required for Alerting Notes. + * Initialize the indices required for Alerting comments. * First check if the index exists, and if not create the index with the provided callback listeners. * * @param actionListener A callback listener for the index creation call. Generally in the form of onSuccess, onFailure @@ -111,21 +113,21 @@ class NotesIndices( fun onMaster() { try { // try to rollover immediately as we might be restarting the cluster - rolloverNotesHistoryIndex() + rolloverCommentsHistoryIndex() // schedule the next rollover for approx MAX_AGE later - scheduledNotesRollover = threadPool - .scheduleWithFixedDelay({ rolloverAndDeleteNotesHistoryIndices() }, notesHistoryRolloverPeriod, executorName()) + scheduledCommentsRollover = threadPool + .scheduleWithFixedDelay({ rolloverAndDeleteCommentsHistoryIndices() }, commentsHistoryRolloverPeriod, executorName()) } catch (e: Exception) { // This should be run on cluster startup logger.error( - "Error creating notes indices. Notes can't be recorded until master node is restarted.", + "Error creating comments indices. Comments can't be recorded until master node is restarted.", e ) } } fun offMaster() { - scheduledNotesRollover?.cancel() + scheduledCommentsRollover?.cancel() } private fun executorName(): String { @@ -146,66 +148,66 @@ class NotesIndices( } // if the indexes have been deleted they need to be reinitialized - notesHistoryIndexInitialized = event.state().metadata().hasAlias(NOTES_HISTORY_WRITE_INDEX) + commentsHistoryIndexInitialized = event.state().metadata().hasAlias(COMMENTS_HISTORY_WRITE_INDEX) } - private fun rescheduleNotesRollover() { + private fun rescheduleCommentsRollover() { if (clusterService.state().nodes.isLocalNodeElectedMaster) { - scheduledNotesRollover?.cancel() - scheduledNotesRollover = threadPool - .scheduleWithFixedDelay({ rolloverAndDeleteNotesHistoryIndices() }, notesHistoryRolloverPeriod, executorName()) + scheduledCommentsRollover?.cancel() + scheduledCommentsRollover = threadPool + .scheduleWithFixedDelay({ rolloverAndDeleteCommentsHistoryIndices() }, commentsHistoryRolloverPeriod, executorName()) } } - fun isNotesHistoryInitialized(): Boolean { - return clusterService.state().metadata.hasAlias(NOTES_HISTORY_WRITE_INDEX) + fun isCommentsHistoryInitialized(): Boolean { + return clusterService.state().metadata.hasAlias(COMMENTS_HISTORY_WRITE_INDEX) } - fun isNotesHistoryEnabled(): Boolean { - return notesHistoryEnabled + fun isCommentsHistoryEnabled(): Boolean { + return commentsHistoryEnabled } - suspend fun createOrUpdateInitialNotesHistoryIndex() { - if (!isNotesHistoryInitialized()) { - notesHistoryIndexInitialized = createIndex(NOTES_HISTORY_INDEX_PATTERN, notesMapping(), NOTES_HISTORY_WRITE_INDEX) - if (notesHistoryIndexInitialized) - IndexUtils.lastUpdatedNotesHistoryIndex = IndexUtils.getIndexNameWithAlias( + suspend fun createOrUpdateInitialCommentsHistoryIndex() { + if (!isCommentsHistoryInitialized()) { + commentsHistoryIndexInitialized = createIndex(COMMENTS_HISTORY_INDEX_PATTERN, commentsMapping(), COMMENTS_HISTORY_WRITE_INDEX) + if (commentsHistoryIndexInitialized) + IndexUtils.lastUpdatedCommentsHistoryIndex = IndexUtils.getIndexNameWithAlias( clusterService.state(), - NOTES_HISTORY_WRITE_INDEX + COMMENTS_HISTORY_WRITE_INDEX ) } else { - updateIndexMapping(NOTES_HISTORY_WRITE_INDEX, notesMapping(), true) + updateIndexMapping(COMMENTS_HISTORY_WRITE_INDEX, commentsMapping(), true) } - notesHistoryIndexInitialized + commentsHistoryIndexInitialized } - private fun rolloverAndDeleteNotesHistoryIndices() { - rolloverNotesHistoryIndex() - deleteOldIndices("Notes", NOTES_HISTORY_ALL) + private fun rolloverAndDeleteCommentsHistoryIndices() { + rolloverCommentsHistoryIndex() + deleteOldIndices("comments", COMMENTS_HISTORY_ALL) } - private fun rolloverNotesHistoryIndex() { + private fun rolloverCommentsHistoryIndex() { rolloverIndex( - notesHistoryIndexInitialized, - NOTES_HISTORY_WRITE_INDEX, - NOTES_HISTORY_INDEX_PATTERN, - notesMapping(), - notesHistoryMaxDocs, - notesHistoryMaxAge, - NOTES_HISTORY_WRITE_INDEX + commentsHistoryIndexInitialized, + COMMENTS_HISTORY_WRITE_INDEX, + COMMENTS_HISTORY_INDEX_PATTERN, + commentsMapping(), + commentsHistoryMaxDocs, + commentsHistoryMaxAge, + COMMENTS_HISTORY_WRITE_INDEX ) } // TODO: Everything below here are util functions straight from AlertIndices.kt // TODO: might need to reuse their code or refactor - // TODO: may merge into AlertIndices.kt if we decide to make notes indices + // TODO: may merge into AlertIndices.kt if we decide to make comments indices // TODO: component-specific instead of universal and component-agnostic private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { val indicesToDelete = mutableListOf() for (entry in clusterStateResponse.state.metadata.indices) { val indexMetaData = entry.value - getHistoryIndexToDelete(indexMetaData, notesHistoryRetentionPeriod.millis, NOTES_HISTORY_WRITE_INDEX, true) + getHistoryIndexToDelete(indexMetaData, commentsHistoryRetentionPeriod.millis, COMMENTS_HISTORY_WRITE_INDEX, true) ?.let { indicesToDelete.add(it) } } return indicesToDelete @@ -224,9 +226,9 @@ class NotesIndices( if (historyEnabled) { // If the index has the write alias and history is enabled, don't delete the index return null - } else if (writeIndex == NOTES_HISTORY_WRITE_INDEX) { - // Otherwise reset notesHistoryIndexInitialized since index will be deleted - notesHistoryIndexInitialized = false + } else if (writeIndex == COMMENTS_HISTORY_WRITE_INDEX) { + // Otherwise reset commentsHistoryIndexInitialized since index will be deleted + commentsHistoryIndexInitialized = false } } @@ -244,14 +246,14 @@ class NotesIndices( override fun onResponse(deleteIndicesResponse: AcknowledgedResponse) { if (!deleteIndicesResponse.isAcknowledged) { logger.error( - "Could not delete one or more Notes history indices: $indicesToDelete." + + "Could not delete one or more comments history indices: $indicesToDelete." + "Retrying one by one." ) deleteOldHistoryIndex(indicesToDelete) } } override fun onFailure(e: Exception) { - logger.error("Delete for Notes History Indices $indicesToDelete Failed. Retrying one By one.") + logger.error("Delete for comments History Indices $indicesToDelete Failed. Retrying one By one.") deleteOldHistoryIndex(indicesToDelete) } } @@ -268,7 +270,7 @@ class NotesIndices( override fun onResponse(acknowledgedResponse: AcknowledgedResponse?) { if (acknowledgedResponse != null) { if (!acknowledgedResponse.isAcknowledged) { - logger.error("Could not delete one or more Notes history indices: $index") + logger.error("Could not delete one or more comments history indices: $index") } } } @@ -314,7 +316,7 @@ class NotesIndices( targetIndex = IndexUtils.getIndexNameWithAlias(clusterState, index) } - if (targetIndex == IndexUtils.lastUpdatedNotesHistoryIndex + if (targetIndex == IndexUtils.lastUpdatedCommentsHistoryIndex ) { return } @@ -332,7 +334,7 @@ class NotesIndices( private fun setIndexUpdateFlag(index: String, targetIndex: String) { when (index) { - NOTES_HISTORY_WRITE_INDEX -> IndexUtils.lastUpdatedNotesHistoryIndex = targetIndex + COMMENTS_HISTORY_WRITE_INDEX -> IndexUtils.lastUpdatedCommentsHistoryIndex = targetIndex } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt index eca024d63..d52eecda4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt @@ -6,8 +6,8 @@ package org.opensearch.alerting.model import org.opensearch.commons.alerting.model.Alert +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.alerting.model.DocLevelQuery -import org.opensearch.commons.alerting.model.Note /** * This model is a wrapper for [Alert] that should only be used to create a more @@ -17,7 +17,7 @@ data class AlertContext( val alert: Alert, val associatedQueries: List? = null, val sampleDocs: List>? = null, - val notes: List? = null + val comments: List? = null ) { fun asTemplateArg(): Map { val queriesContext = associatedQueries?.map { @@ -28,12 +28,12 @@ data class AlertContext( ) } - val notesContext = notes?.map { + val commentsContext = comments?.map { mapOf( - Note.NOTE_CREATED_TIME_FIELD to it.createdTime, - Note.NOTE_LAST_UPDATED_TIME_FIELD to it.lastUpdatedTime, - Note.NOTE_CONTENT_FIELD to it.content, - Note.NOTE_USER_FIELD to it.user + Comment.COMMENT_CREATED_TIME_FIELD to it.createdTime, + Comment.COMMENT_LAST_UPDATED_TIME_FIELD to it.lastUpdatedTime, + Comment.COMMENT_CONTENT_FIELD to it.content, + Comment.COMMENT_USER_FIELD to it.user ) } @@ -41,7 +41,7 @@ data class AlertContext( val customContextFields = mapOf( ASSOCIATED_QUERIES_FIELD to queriesContext, SAMPLE_DOCS_FIELD to sampleDocs, - NOTES_FIELD to notesContext + COMMENTS_FIELD to commentsContext ) // Get the alert template args @@ -57,6 +57,6 @@ data class AlertContext( companion object { const val ASSOCIATED_QUERIES_FIELD = "associated_queries" const val SAMPLE_DOCS_FIELD = "sample_documents" - const val NOTES_FIELD = "notes" + const val COMMENTS_FIELD = "comments" } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt similarity index 65% rename from alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt index a38402a63..1a33888ea 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt @@ -10,7 +10,7 @@ import org.apache.logging.log4j.Logger import org.opensearch.alerting.AlertingPlugin import org.opensearch.client.node.NodeClient import org.opensearch.commons.alerting.action.AlertingActions -import org.opensearch.commons.alerting.action.DeleteNoteRequest +import org.opensearch.commons.alerting.action.DeleteCommentRequest import org.opensearch.rest.BaseRestHandler import org.opensearch.rest.RestHandler.Route import org.opensearch.rest.RestRequest @@ -20,30 +20,30 @@ import java.io.IOException private val log: Logger = LogManager.getLogger(RestDeleteMonitorAction::class.java) /** - * Rest handlers to create and update notes. + * Rest handlers to create and update comments. */ -class RestDeleteNoteAction : BaseRestHandler() { +class RestDeleteAlertingCommentAction : BaseRestHandler() { override fun getName(): String { - return "delete_note_action" + return "delete_alerting_comment_action" } override fun routes(): List { return listOf( Route( RestRequest.Method.DELETE, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}" + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/{commentID}" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}") - val noteId = request.param("noteID") - val deleteMonitorRequest = DeleteNoteRequest(noteId) + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/{commentID}") + val commentId = request.param("commentID") + val deleteMonitorRequest = DeleteCommentRequest(commentId) return RestChannelConsumer { channel -> - client.execute(AlertingActions.DELETE_NOTES_ACTION_TYPE, deleteMonitorRequest, RestToXContentListener(channel)) + client.execute(AlertingActions.DELETE_COMMENT_ACTION_TYPE, deleteMonitorRequest, RestToXContentListener(channel)) } } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt similarity index 71% rename from alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index 1d4b996b8..1f6476b0e 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -12,10 +12,10 @@ import org.opensearch.alerting.util.IF_PRIMARY_TERM import org.opensearch.alerting.util.IF_SEQ_NO import org.opensearch.client.node.NodeClient import org.opensearch.commons.alerting.action.AlertingActions -import org.opensearch.commons.alerting.action.IndexNoteRequest -import org.opensearch.commons.alerting.action.IndexNoteResponse +import org.opensearch.commons.alerting.action.IndexCommentRequest +import org.opensearch.commons.alerting.action.IndexCommentResponse import org.opensearch.commons.alerting.model.Alert -import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.alerting.model.Comment import org.opensearch.core.rest.RestStatus import org.opensearch.core.xcontent.ToXContent import org.opensearch.index.seqno.SequenceNumbers @@ -31,65 +31,65 @@ import java.io.IOException private val log = LogManager.getLogger(RestIndexMonitorAction::class.java) /** - * Rest handlers to create and update notes. + * Rest handlers to create and update alerting comments. */ -class RestIndexNoteAction : BaseRestHandler() { +class RestIndexAlertingCommentAction : BaseRestHandler() { override fun getName(): String { - return "index_note_action" + return "index_alerting_comment_action" } override fun routes(): List { return listOf( Route( RestRequest.Method.POST, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/{alertID}/notes" + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/{alertID}/comments" ), Route( RestRequest.Method.PUT, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/{noteID}" + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/{commentID}" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes") + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments") val alertId = request.param("alertID", Alert.NO_ID) - val noteId = request.param("noteID", Note.NO_ID) + val commentId = request.param("commentID", Comment.NO_ID) if (request.method() == RestRequest.Method.POST && Alert.NO_ID == alertId) { throw AlertingException.wrap(IllegalArgumentException("Missing alert ID")) - } else if (request.method() == RestRequest.Method.PUT && Note.NO_ID == noteId) { - throw AlertingException.wrap(IllegalArgumentException("Missing note ID")) + } else if (request.method() == RestRequest.Method.PUT && Comment.NO_ID == commentId) { + throw AlertingException.wrap(IllegalArgumentException("Missing comment ID")) } val content = request.contentParser().map()["content"] as String? if (content.isNullOrEmpty()) { - throw AlertingException.wrap(IllegalArgumentException("Missing note content")) + throw AlertingException.wrap(IllegalArgumentException("Missing comment content")) } val seqNo = request.paramAsLong(IF_SEQ_NO, SequenceNumbers.UNASSIGNED_SEQ_NO) val primaryTerm = request.paramAsLong(IF_PRIMARY_TERM, SequenceNumbers.UNASSIGNED_PRIMARY_TERM) - val indexNoteRequest = IndexNoteRequest(alertId, noteId, seqNo, primaryTerm, request.method(), content) + val indexCommentRequest = IndexCommentRequest(alertId, commentId, seqNo, primaryTerm, request.method(), content) return RestChannelConsumer { channel -> - client.execute(AlertingActions.INDEX_NOTE_ACTION_TYPE, indexNoteRequest, indexNoteResponse(channel, request.method())) + client.execute(AlertingActions.INDEX_COMMENT_ACTION_TYPE, indexCommentRequest, indexCommentResponse(channel, request.method())) } } - private fun indexNoteResponse(channel: RestChannel, restMethod: RestRequest.Method): - RestResponseListener { - return object : RestResponseListener(channel) { + private fun indexCommentResponse(channel: RestChannel, restMethod: RestRequest.Method): + RestResponseListener { + return object : RestResponseListener(channel) { @Throws(Exception::class) - override fun buildResponse(response: IndexNoteResponse): RestResponse { + override fun buildResponse(response: IndexCommentResponse): RestResponse { var returnStatus = RestStatus.CREATED if (restMethod == RestRequest.Method.PUT) returnStatus = RestStatus.OK val restResponse = BytesRestResponse(returnStatus, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) if (returnStatus == RestStatus.CREATED) { - val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/${response.id}" + val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/${response.id}" restResponse.addHeader("Location", location) } return restResponse diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt similarity index 74% rename from alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt index eb6ce9342..ef073df7b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt @@ -9,15 +9,15 @@ import org.apache.logging.log4j.LogManager import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse import org.opensearch.alerting.AlertingPlugin -import org.opensearch.alerting.notes.NotesIndices.Companion.ALL_NOTES_INDEX_PATTERN +import org.opensearch.alerting.comments.CommentsIndices.Companion.ALL_COMMENTS_INDEX_PATTERN import org.opensearch.alerting.util.context import org.opensearch.client.node.NodeClient import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.XContentFactory.jsonBuilder import org.opensearch.common.xcontent.XContentType import org.opensearch.commons.alerting.action.AlertingActions -import org.opensearch.commons.alerting.action.SearchNoteRequest -import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.alerting.action.SearchCommentRequest +import org.opensearch.commons.alerting.model.Comment import org.opensearch.core.common.bytes.BytesReference import org.opensearch.core.rest.RestStatus import org.opensearch.core.xcontent.ToXContent.EMPTY_PARAMS @@ -34,26 +34,26 @@ import java.io.IOException private val log = LogManager.getLogger(RestIndexMonitorAction::class.java) /** - * Rest handler to search notes. + * Rest handler to search alerting comments. */ -class RestSearchNoteAction() : BaseRestHandler() { +class RestSearchAlertingCommentAction() : BaseRestHandler() { override fun getName(): String { - return "search_notes_action" + return "search_alerting_comments_action" } override fun routes(): List { return listOf( Route( RestRequest.Method.GET, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/_search" + "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/_search" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/notes/_search") + log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/_search") val searchSourceBuilder = SearchSourceBuilder() searchSourceBuilder.parseXContent(request.contentOrSourceParamParser()) @@ -61,15 +61,15 @@ class RestSearchNoteAction() : BaseRestHandler() { val searchRequest = SearchRequest() .source(searchSourceBuilder) - .indices(ALL_NOTES_INDEX_PATTERN) + .indices(ALL_COMMENTS_INDEX_PATTERN) - val searchNoteRequest = SearchNoteRequest(searchRequest) + val searchCommentRequest = SearchCommentRequest(searchRequest) return RestChannelConsumer { channel -> - client.execute(AlertingActions.SEARCH_NOTES_ACTION_TYPE, searchNoteRequest, searchNoteResponse(channel)) + client.execute(AlertingActions.SEARCH_COMMENTS_ACTION_TYPE, searchCommentRequest, searchCommentResponse(channel)) } } - private fun searchNoteResponse(channel: RestChannel): RestResponseListener { + private fun searchCommentResponse(channel: RestChannel): RestResponseListener { return object : RestResponseListener(channel) { @Throws(Exception::class) override fun buildResponse(response: SearchResponse): RestResponse { @@ -86,13 +86,13 @@ class RestSearchNoteAction() : BaseRestHandler() { hit.sourceAsString ).use { hitsParser -> hitsParser.nextToken() - val note = Note.parse(hitsParser, hit.id) - val xcb = note.toXContent(jsonBuilder(), EMPTY_PARAMS) + val comment = Comment.parse(hitsParser, hit.id) + val xcb = comment.toXContent(jsonBuilder(), EMPTY_PARAMS) hit.sourceRef(BytesReference.bytes(xcb)) } } } catch (e: Exception) { - log.info("The note parsing failed. Will return response as is.") + log.info("The comment parsing failed. Will return response as is.") } return BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), EMPTY_PARAMS)) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index 0ce4924ff..9f4212370 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -224,59 +224,59 @@ class AlertingSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ) - val ALERTING_NOTES_ENABLED = Setting.boolSetting( - "plugins.alerting.notes_enabled", + val ALERTING_COMMENTS_ENABLED = Setting.boolSetting( + "plugins.alerting.comments_enabled", false, Setting.Property.NodeScope, Setting.Property.Dynamic ) - val NOTES_HISTORY_ENABLED = Setting.boolSetting( - "plugins.alerting.notes_history_enabled", + val COMMENTS_HISTORY_ENABLED = Setting.boolSetting( + "plugins.alerting.comments_history_enabled", true, Setting.Property.NodeScope, Setting.Property.Dynamic ) - val NOTES_HISTORY_MAX_DOCS = Setting.longSetting( - "plugins.alerting.notes_history_max_docs", + val COMMENTS_HISTORY_MAX_DOCS = Setting.longSetting( + "plugins.alerting.comments_history_max_docs", 1000L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic ) - val NOTES_HISTORY_INDEX_MAX_AGE = Setting.positiveTimeSetting( - "plugins.alerting.notes_history_max_age", + val COMMENTS_HISTORY_INDEX_MAX_AGE = Setting.positiveTimeSetting( + "plugins.alerting.comments_history_max_age", TimeValue(30, TimeUnit.DAYS), Setting.Property.NodeScope, Setting.Property.Dynamic ) - val NOTES_HISTORY_ROLLOVER_PERIOD = Setting.positiveTimeSetting( - "plugins.alerting.notes_history_rollover_period", + val COMMENTS_HISTORY_ROLLOVER_PERIOD = Setting.positiveTimeSetting( + "plugins.alerting.comments_history_rollover_period", TimeValue(12, TimeUnit.HOURS), Setting.Property.NodeScope, Setting.Property.Dynamic ) - val NOTES_HISTORY_RETENTION_PERIOD = Setting.positiveTimeSetting( - "plugins.alerting.notes_history_retention_period", + val COMMENTS_HISTORY_RETENTION_PERIOD = Setting.positiveTimeSetting( + "plugins.alerting.comments_history_retention_period", TimeValue(60, TimeUnit.DAYS), Setting.Property.NodeScope, Setting.Property.Dynamic ) - val NOTES_MAX_CONTENT_SIZE = Setting.longSetting( - "plugins.alerting.notes.max_content_size", + val COMMENTS_MAX_CONTENT_SIZE = Setting.longSetting( + "plugins.alerting.comments.max_content_size", 2000L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic ) - val MAX_NOTES_PER_ALERT = Setting.longSetting( - "plugins.alerting.notes.max_notes_per_alert", + val MAX_COMMENTS_PER_ALERT = Setting.longSetting( + "plugins.alerting.comments.max_comments_per_alert", 500L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic ) - val MAX_NOTES_PER_NOTIFICATION = Setting.intSetting( - "plugins.alerting.notes.max_notes_per_notification", + val MAX_COMMENTS_PER_NOTIFICATION = Setting.intSetting( + "plugins.alerting.comments.max_comments_per_notification", 3, 0, Setting.Property.NodeScope, Setting.Property.Dynamic diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt similarity index 65% rename from alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index f947ec857..94b2a52a3 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -16,7 +16,7 @@ import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction -import org.opensearch.alerting.notes.NotesIndices.Companion.ALL_NOTES_INDEX_PATTERN +import org.opensearch.alerting.comments.CommentsIndices.Companion.ALL_COMMENTS_INDEX_PATTERN import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.util.AlertingException @@ -28,9 +28,9 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.commons.alerting.action.AlertingActions -import org.opensearch.commons.alerting.action.DeleteNoteRequest -import org.opensearch.commons.alerting.action.DeleteNoteResponse -import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.alerting.action.DeleteCommentRequest +import org.opensearch.commons.alerting.action.DeleteCommentResponse +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener @@ -44,41 +44,43 @@ import org.opensearch.tasks.Task import org.opensearch.transport.TransportService private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) -private val log = LogManager.getLogger(TransportDeleteNoteAction::class.java) +private val log = LogManager.getLogger(TransportDeleteAlertingCommentAction::class.java) -class TransportDeleteNoteAction @Inject constructor( +class TransportDeleteAlertingCommentAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, val clusterService: ClusterService, settings: Settings, val xContentRegistry: NamedXContentRegistry -) : HandledTransportAction( - AlertingActions.DELETE_NOTES_ACTION_NAME, transportService, actionFilters, ::DeleteNoteRequest +) : HandledTransportAction( + AlertingActions.DELETE_COMMENT_ACTION_NAME, transportService, actionFilters, ::DeleteCommentRequest ), SecureTransportAction { - @Volatile private var alertingNotesEnabled = AlertingSettings.ALERTING_NOTES_ENABLED.get(settings) + @Volatile private var alertingCommentsEnabled = AlertingSettings.ALERTING_COMMENTS_ENABLED.get(settings) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_NOTES_ENABLED) { alertingNotesEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_COMMENTS_ENABLED) { + alertingCommentsEnabled = it + } listenFilterBySettingChange(clusterService) } - override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { + override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { // validate feature flag enabled - if (!alertingNotesEnabled) { + if (!alertingCommentsEnabled) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Notes for Alerting is currently disabled", RestStatus.FORBIDDEN), + OpenSearchStatusException("Comments for Alerting is currently disabled", RestStatus.FORBIDDEN), ) ) return } - val transformedRequest = request as? DeleteNoteRequest - ?: recreateObject(request) { DeleteNoteRequest(it) } + val transformedRequest = request as? DeleteCommentRequest + ?: recreateObject(request) { DeleteCommentRequest(it) } val user = readUserFromThreadContext(client) @@ -86,62 +88,62 @@ class TransportDeleteNoteAction @Inject constructor( return } scope.launch { - DeleteNoteHandler( + DeleteCommentHandler( client, actionListener, user, - transformedRequest.noteId + transformedRequest.commentId ).resolveUserAndStart() } } - inner class DeleteNoteHandler( + inner class DeleteCommentHandler( private val client: Client, - private val actionListener: ActionListener, + private val actionListener: ActionListener, private val user: User?, - private val noteId: String + private val commentId: String ) { private var sourceIndex: String? = null suspend fun resolveUserAndStart() { try { - val note = getNote() + val comment = getComment() if (sourceIndex == null) { actionListener.onFailure( AlertingException( - "Could not resolve the index the given Note came from", + "Could not resolve the index the given Comment came from", RestStatus.INTERNAL_SERVER_ERROR, IllegalStateException() ) ) } - // if user is null because security plugin is not installed, anyone can delete any note - // otherwise, only allow note deletion if the deletion requester is the same as the note's author - val canDelete = user == null || user.name == note.user?.name || isAdmin(user) + // if user is null because security plugin is not installed, anyone can delete any comment + // otherwise, only allow comment deletion if the deletion requester is the same as the comment's author + val canDelete = user == null || user.name == comment.user?.name || isAdmin(user) - val deleteRequest = DeleteRequest(sourceIndex, noteId) + val deleteRequest = DeleteRequest(sourceIndex, commentId) if (canDelete) { - log.debug("Deleting the note with id ${deleteRequest.id()}") + log.debug("Deleting the comment with id ${deleteRequest.id()}") val deleteResponse = client.suspendUntil { delete(deleteRequest, it) } - actionListener.onResponse(DeleteNoteResponse(deleteResponse.id)) + actionListener.onResponse(DeleteCommentResponse(deleteResponse.id)) } else { actionListener.onFailure( - AlertingException("Not allowed to delete this note!", RestStatus.FORBIDDEN, IllegalStateException()) + AlertingException("Not allowed to delete this comment!", RestStatus.FORBIDDEN, IllegalStateException()) ) } } catch (t: Exception) { - log.error("Failed to delete note $noteId", t) + log.error("Failed to delete comment $commentId", t) actionListener.onFailure(AlertingException.wrap(t)) } } - private suspend fun getNote(): Note { + private suspend fun getComment(): Comment { val queryBuilder = QueryBuilders .boolQuery() - .must(QueryBuilders.termsQuery("_id", noteId)) + .must(QueryBuilders.termsQuery("_id", commentId)) val searchSourceBuilder = SearchSourceBuilder() .version(true) @@ -149,17 +151,17 @@ class TransportDeleteNoteAction @Inject constructor( .query(queryBuilder) val searchRequest = SearchRequest() .source(searchSourceBuilder) - .indices(ALL_NOTES_INDEX_PATTERN) + .indices(ALL_COMMENTS_INDEX_PATTERN) val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } if (searchResponse.hits.totalHits.value == 0L) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Note with $noteId is not found", RestStatus.NOT_FOUND) + OpenSearchStatusException("Comment with $commentId is not found", RestStatus.NOT_FOUND) ) ) } - val notes = searchResponse.hits.map { hit -> + val comments = searchResponse.hits.map { hit -> val xcp = XContentHelper.createParser( NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, @@ -167,12 +169,12 @@ class TransportDeleteNoteAction @Inject constructor( XContentType.JSON ) XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) - val note = Note.parse(xcp, hit.id) + val comment = Comment.parse(xcp, hit.id) sourceIndex = hit.index - note + comment } - return notes[0] // we searched on Note ID, there should only be one Note in the List + return comments[0] // we searched on Comment ID, there should only be one Comment in the List } } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt similarity index 69% rename from alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index 799193bf6..a22c255b7 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -20,16 +20,16 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.alerting.alerts.AlertIndices -import org.opensearch.alerting.notes.NotesIndices -import org.opensearch.alerting.notes.NotesIndices.Companion.NOTES_HISTORY_WRITE_INDEX +import org.opensearch.alerting.comments.CommentsIndices +import org.opensearch.alerting.comments.CommentsIndices.Companion.COMMENTS_HISTORY_WRITE_INDEX import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.settings.AlertingSettings -import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_NOTES_ENABLED +import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_COMMENTS_ENABLED +import org.opensearch.alerting.settings.AlertingSettings.Companion.COMMENTS_MAX_CONTENT_SIZE import org.opensearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT -import org.opensearch.alerting.settings.AlertingSettings.Companion.MAX_NOTES_PER_ALERT -import org.opensearch.alerting.settings.AlertingSettings.Companion.NOTES_MAX_CONTENT_SIZE +import org.opensearch.alerting.settings.AlertingSettings.Companion.MAX_COMMENTS_PER_ALERT import org.opensearch.alerting.util.AlertingException -import org.opensearch.alerting.util.NotesUtils +import org.opensearch.alerting.util.CommentsUtils import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -39,10 +39,10 @@ import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.commons.alerting.action.AlertingActions -import org.opensearch.commons.alerting.action.IndexNoteRequest -import org.opensearch.commons.alerting.action.IndexNoteResponse +import org.opensearch.commons.alerting.action.IndexCommentRequest +import org.opensearch.commons.alerting.action.IndexCommentResponse import org.opensearch.commons.alerting.model.Alert -import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener @@ -62,36 +62,36 @@ import java.time.Instant private val log = LogManager.getLogger(TransportIndexMonitorAction::class.java) private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) -class TransportIndexNoteAction +class TransportIndexAlertingCommentAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, - val notesIndices: NotesIndices, + val commentsIndices: CommentsIndices, val clusterService: ClusterService, val settings: Settings, val xContentRegistry: NamedXContentRegistry, val namedWriteableRegistry: NamedWriteableRegistry, -) : HandledTransportAction( - AlertingActions.INDEX_NOTE_ACTION_NAME, +) : HandledTransportAction( + AlertingActions.INDEX_COMMENT_ACTION_NAME, transportService, actionFilters, - ::IndexNoteRequest, + ::IndexCommentRequest, ), SecureTransportAction { - @Volatile private var alertingNotesEnabled = ALERTING_NOTES_ENABLED.get(settings) - @Volatile private var notesMaxContentSize = NOTES_MAX_CONTENT_SIZE.get(settings) - @Volatile private var maxNotesPerAlert = MAX_NOTES_PER_ALERT.get(settings) + @Volatile private var alertingCommentsEnabled = ALERTING_COMMENTS_ENABLED.get(settings) + @Volatile private var commentsMaxContentSize = COMMENTS_MAX_CONTENT_SIZE.get(settings) + @Volatile private var maxCommentsPerAlert = MAX_COMMENTS_PER_ALERT.get(settings) @Volatile private var indexTimeout = INDEX_TIMEOUT.get(settings) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_NOTES_ENABLED) { alertingNotesEnabled = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(NOTES_MAX_CONTENT_SIZE) { notesMaxContentSize = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_NOTES_PER_ALERT) { maxNotesPerAlert = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_COMMENTS_ENABLED) { alertingCommentsEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(COMMENTS_MAX_CONTENT_SIZE) { commentsMaxContentSize = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_COMMENTS_PER_ALERT) { maxCommentsPerAlert = it } clusterService.clusterSettings.addSettingsUpdateConsumer(INDEX_TIMEOUT) { indexTimeout = it } listenFilterBySettingChange(clusterService) } @@ -99,29 +99,29 @@ constructor( override fun doExecute( task: Task, request: ActionRequest, - actionListener: ActionListener, + actionListener: ActionListener, ) { // validate feature flag enabled - if (!alertingNotesEnabled) { + if (!alertingCommentsEnabled) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Notes for Alerting is currently disabled", RestStatus.FORBIDDEN), + OpenSearchStatusException("Comments for Alerting is currently disabled", RestStatus.FORBIDDEN), ) ) return } val transformedRequest = - request as? IndexNoteRequest + request as? IndexCommentRequest ?: recreateObject(request, namedWriteableRegistry) { - IndexNoteRequest(it) + IndexCommentRequest(it) } - // validate note content size - if (transformedRequest.content.length > notesMaxContentSize) { + // validate comment content size + if (transformedRequest.content.length > commentsMaxContentSize) { actionListener.onFailure( AlertingException.wrap( - IllegalArgumentException("Note content exceeds max length of $notesMaxContentSize characters"), + IllegalArgumentException("Comment content exceeds max length of $commentsMaxContentSize characters"), ) ) return @@ -131,29 +131,29 @@ constructor( client.threadPool().threadContext.stashContext().use { scope.launch { - IndexNoteHandler(client, actionListener, transformedRequest, user).start() + IndexCommentHandler(client, actionListener, transformedRequest, user).start() } } } - inner class IndexNoteHandler( + inner class IndexCommentHandler( private val client: Client, - private val actionListener: ActionListener, - private val request: IndexNoteRequest, + private val actionListener: ActionListener, + private val request: IndexCommentRequest, private val user: User?, ) { suspend fun start() { - notesIndices.createOrUpdateInitialNotesHistoryIndex() + commentsIndices.createOrUpdateInitialCommentsHistoryIndex() if (request.method == RestRequest.Method.PUT) { - updateNote() + updateComment() } else { - indexNote() + indexComment() } } - private suspend fun indexNote() { - // need to validate the existence of the Alert that user is trying to add Note to. - // Also need to check if user has permissions to add a Note to the passed in Alert. To do this, + private suspend fun indexComment() { + // need to validate the existence of the Alert that user is trying to add Comment to. + // Also need to check if user has permissions to add a Comment to the passed in Alert. To do this, // we retrieve the Alert to get its associated monitor user, and use that to // check if they have permissions to the Monitor that generated the Alert val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.entityId))) @@ -163,7 +163,7 @@ constructor( .seqNoAndPrimaryTerm(true) .query(queryBuilder) - // search all alerts, since user might want to create a note + // search all alerts, since user might want to create a comment // on a completed alert val searchRequest = SearchRequest() @@ -194,32 +194,32 @@ constructor( val alert = alerts[0] // there should only be 1 Alert that matched the request alert ID - val numNotesOnThisAlert = NotesUtils.getNoteIDsByAlertIDs(client, listOf(alert.id)).size - if (numNotesOnThisAlert >= maxNotesPerAlert) { + val numCommentsOnThisAlert = CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id)).size + if (numCommentsOnThisAlert >= maxCommentsPerAlert) { actionListener.onFailure( AlertingException.wrap( IllegalArgumentException( - "This request would create more than the allowed number of Notes" + - "for this Alert: $maxNotesPerAlert" + "This request would create more than the allowed number of Comments" + + "for this Alert: $maxCommentsPerAlert" ) ) ) return } - log.info("checking user permissions in index note") + log.info("checking user permissions in index comment") checkUserPermissionsWithResource(user, alert.monitorUser, actionListener, "monitor", alert.monitorId) - val note = Note(entityId = request.entityId, content = request.content, createdTime = Instant.now(), user = user) + val comment = Comment(entityId = request.entityId, content = request.content, createdTime = Instant.now(), user = user) val indexRequest = - IndexRequest(NOTES_HISTORY_WRITE_INDEX) - .source(note.toXContentWithUser(XContentFactory.jsonBuilder())) + IndexRequest(COMMENTS_HISTORY_WRITE_INDEX) + .source(comment.toXContentWithUser(XContentFactory.jsonBuilder())) .setIfSeqNo(request.seqNo) .setIfPrimaryTerm(request.primaryTerm) .timeout(indexTimeout) - log.info("Creating new note: ${note.toXContentWithUser(XContentFactory.jsonBuilder())}") + log.info("Creating new comment: ${comment.toXContentWithUser(XContentFactory.jsonBuilder())}") try { val indexResponse: IndexResponse = client.suspendUntil { client.index(indexRequest, it) } @@ -232,21 +232,21 @@ constructor( } actionListener.onResponse( - IndexNoteResponse(indexResponse.id, indexResponse.seqNo, indexResponse.primaryTerm, note) + IndexCommentResponse(indexResponse.id, indexResponse.seqNo, indexResponse.primaryTerm, comment) ) } catch (t: Exception) { actionListener.onFailure(AlertingException.wrap(t)) } } - private suspend fun updateNote() { - val getRequest = GetRequest(NOTES_HISTORY_WRITE_INDEX, request.noteId) + private suspend fun updateComment() { + val getRequest = GetRequest(COMMENTS_HISTORY_WRITE_INDEX, request.commentId) try { val getResponse: GetResponse = client.suspendUntil { client.get(getRequest, it) } if (!getResponse.isExists) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Note with ${request.noteId} is not found", RestStatus.NOT_FOUND), + OpenSearchStatusException("Comment with ${request.commentId} is not found", RestStatus.NOT_FOUND), ), ) return @@ -259,24 +259,24 @@ constructor( XContentType.JSON, ) xcp.nextToken() - val note = Note.parse(xcp, getResponse.id) - log.info("note to be updated: $note") - onGetNoteResponse(note) + val comment = Comment.parse(xcp, getResponse.id) + log.info("comment to be updated: $comment") + onGetCommentResponse(comment) } catch (t: Exception) { actionListener.onFailure(AlertingException.wrap(t)) } } - private suspend fun onGetNoteResponse(currentNote: Note) { - // check that the user has permissions to edit the note. user can edit note if + private suspend fun onGetCommentResponse(currentComment: Comment) { + // check that the user has permissions to edit the comment. user can edit comment if // - user is Admin - // - user is the author of the note - if (user != null && !isAdmin(user) && user.name != currentNote.user?.name) { + // - user is the author of the comment + if (user != null && !isAdmin(user) && user.name != currentComment.user?.name) { actionListener.onFailure( AlertingException.wrap( OpenSearchStatusException( - "Note ${request.noteId} created by ${currentNote.user} " + - "can only be edited by Admin or ${currentNote.user} ", + "Comment ${request.commentId} created by ${currentComment.user} " + + "can only be edited by Admin or ${currentComment.user} ", RestStatus.FORBIDDEN, ), ), @@ -284,21 +284,21 @@ constructor( return } - // retains everything from the original note except content and lastUpdatedTime - val requestNote = currentNote.copy(content = request.content, lastUpdatedTime = Instant.now()) + // retains everything from the original comment except content and lastUpdatedTime + val requestComment = currentComment.copy(content = request.content, lastUpdatedTime = Instant.now()) val indexRequest = - IndexRequest(NOTES_HISTORY_WRITE_INDEX) - .source(requestNote.toXContentWithUser(XContentFactory.jsonBuilder())) - .id(requestNote.id) + IndexRequest(COMMENTS_HISTORY_WRITE_INDEX) + .source(requestComment.toXContentWithUser(XContentFactory.jsonBuilder())) + .id(requestComment.id) .setIfSeqNo(request.seqNo) .setIfPrimaryTerm(request.primaryTerm) .timeout(indexTimeout) log.info( - "Updating note, ${currentNote.id}, from: " + - "${currentNote.content} to: " + - requestNote.content, + "Updating comment, ${currentComment.id}, from: " + + "${currentComment.content} to: " + + requestComment.content, ) try { @@ -312,11 +312,11 @@ constructor( } actionListener.onResponse( - IndexNoteResponse( + IndexCommentResponse( indexResponse.id, indexResponse.seqNo, indexResponse.primaryTerm, - requestNote, + requestComment, ), ) } catch (t: Exception) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt similarity index 80% rename from alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt index 774e924a4..549725c40 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchNoteAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt @@ -28,9 +28,9 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.commons.alerting.action.AlertingActions -import org.opensearch.commons.alerting.action.SearchNoteRequest +import org.opensearch.commons.alerting.action.SearchCommentRequest import org.opensearch.commons.alerting.model.Alert -import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener @@ -46,10 +46,10 @@ import org.opensearch.tasks.Task import org.opensearch.transport.TransportService import java.io.IOException -private val log = LogManager.getLogger(TransportSearchMonitorAction::class.java) +private val log = LogManager.getLogger(TransportSearchAlertingCommentAction::class.java) private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) -class TransportSearchNoteAction @Inject constructor( +class TransportSearchAlertingCommentAction @Inject constructor( transportService: TransportService, val settings: Settings, val client: Client, @@ -57,31 +57,33 @@ class TransportSearchNoteAction @Inject constructor( actionFilters: ActionFilters, val namedWriteableRegistry: NamedWriteableRegistry ) : HandledTransportAction( - AlertingActions.SEARCH_NOTES_ACTION_NAME, transportService, actionFilters, ::SearchRequest + AlertingActions.SEARCH_COMMENTS_ACTION_NAME, transportService, actionFilters, ::SearchRequest ), SecureTransportAction { - @Volatile private var alertingNotesEnabled = AlertingSettings.ALERTING_NOTES_ENABLED.get(settings) + @Volatile private var alertingCommentsEnabled = AlertingSettings.ALERTING_COMMENTS_ENABLED.get(settings) @Volatile override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_NOTES_ENABLED) { alertingNotesEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_COMMENTS_ENABLED) { + alertingCommentsEnabled = it + } listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: ActionRequest, actionListener: ActionListener) { // validate feature flag enabled - if (!alertingNotesEnabled) { + if (!alertingCommentsEnabled) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Notes for Alerting is currently disabled", RestStatus.FORBIDDEN), + OpenSearchStatusException("Comments for Alerting is currently disabled", RestStatus.FORBIDDEN), ) ) return } - val transformedRequest = request as? SearchNoteRequest + val transformedRequest = request as? SearchCommentRequest ?: recreateObject(request, namedWriteableRegistry) { - SearchNoteRequest(it) + SearchCommentRequest(it) } val searchSourceBuilder = transformedRequest.searchRequest.source() @@ -102,13 +104,13 @@ class TransportSearchNoteAction @Inject constructor( } } - suspend fun resolve(searchNoteRequest: SearchNoteRequest, actionListener: ActionListener, user: User?) { + suspend fun resolve(searchCommentRequest: SearchCommentRequest, actionListener: ActionListener, user: User?) { if (user == null) { // user is null when: 1/ security is disabled. 2/when user is super-admin. - search(searchNoteRequest.searchRequest, actionListener) + search(searchCommentRequest.searchRequest, actionListener) } else if (!doFilterForUser(user)) { // security is enabled and filterby is disabled. - search(searchNoteRequest.searchRequest, actionListener) + search(searchCommentRequest.searchRequest, actionListener) } else { // security is enabled and filterby is enabled. try { @@ -117,15 +119,15 @@ class TransportSearchNoteAction @Inject constructor( // first retrieve all Alert IDs current User can see after filtering by backend roles val alertIDs = getFilteredAlertIDs(user) - // then filter the returned Notes based on the Alert IDs they're allowed to see - val queryBuilder = searchNoteRequest.searchRequest.source().query() as BoolQueryBuilder - searchNoteRequest.searchRequest.source().query( + // then filter the returned Comments based on the Alert IDs they're allowed to see + val queryBuilder = searchCommentRequest.searchRequest.source().query() as BoolQueryBuilder + searchCommentRequest.searchRequest.source().query( queryBuilder.filter( - QueryBuilders.termsQuery(Note.ENTITY_ID_FIELD, alertIDs) + QueryBuilders.termsQuery(Comment.ENTITY_ID_FIELD, alertIDs) ) ) - search(searchNoteRequest.searchRequest, actionListener) + search(searchCommentRequest.searchRequest, actionListener) } catch (ex: IOException) { actionListener.onFailure(AlertingException.wrap(ex)) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt similarity index 66% rename from alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt rename to alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt index cefccb3b1..ef5ccce59 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/NotesUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt @@ -8,13 +8,13 @@ package org.opensearch.alerting.util import org.apache.logging.log4j.LogManager import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse -import org.opensearch.alerting.notes.NotesIndices.Companion.ALL_NOTES_INDEX_PATTERN +import org.opensearch.alerting.comments.CommentsIndices.Companion.ALL_COMMENTS_INDEX_PATTERN import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.client.Client import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.alerting.model.Note +import org.opensearch.commons.alerting.model.Comment import org.opensearch.core.action.ActionListener import org.opensearch.core.xcontent.NamedXContentRegistry import org.opensearch.core.xcontent.XContentParser @@ -28,17 +28,17 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -private val log = LogManager.getLogger(NotesUtils::class.java) +private val log = LogManager.getLogger(CommentsUtils::class.java) -class NotesUtils { +class CommentsUtils { companion object { - // Deletes all Notes given by the list of Notes IDs - suspend fun deleteNotes(client: Client, noteIDs: List) { - if (noteIDs.isEmpty()) return + // Deletes all Comments given by the list of Comments IDs + suspend fun deleteComments(client: Client, commentIDs: List) { + if (commentIDs.isEmpty()) return val deleteResponse: BulkByScrollResponse = suspendCoroutine { cont -> DeleteByQueryRequestBuilder(client, DeleteByQueryAction.INSTANCE) - .source(ALL_NOTES_INDEX_PATTERN) - .filter(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", noteIDs))) + .source(ALL_COMMENTS_INDEX_PATTERN) + .filter(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", commentIDs))) .refresh(true) .execute( object : ActionListener { @@ -48,14 +48,14 @@ class NotesUtils { ) } deleteResponse.bulkFailures.forEach { - log.error("Failed to delete Note. Note ID: [${it.id}] cause: [${it.cause}] ") + log.error("Failed to delete Comment. Comment ID: [${it.id}] cause: [${it.cause}] ") } } - // Searches through all Notes history indices and returns a list of all Notes associated + // Searches through all Comments history indices and returns a list of all Comments associated // with the Entities given by the list of Entity IDs // TODO: change this to EntityIDs - suspend fun getNotesByAlertIDs(client: Client, alertIDs: List): List { + suspend fun getCommentsByAlertIDs(client: Client, alertIDs: List): List { val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("alert_id", alertIDs)) val searchSourceBuilder = SearchSourceBuilder() @@ -65,11 +65,11 @@ class NotesUtils { val searchRequest = SearchRequest() - .indices(ALL_NOTES_INDEX_PATTERN) + .indices(ALL_COMMENTS_INDEX_PATTERN) .source(searchSourceBuilder) val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } - val notes = searchResponse.hits.map { hit -> + val comments = searchResponse.hits.map { hit -> val xcp = XContentHelper.createParser( NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, @@ -77,19 +77,19 @@ class NotesUtils { XContentType.JSON ) XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) - val note = Note.parse(xcp, hit.id) - note + val comment = Comment.parse(xcp, hit.id) + comment } - return notes + return comments } - // Identical to getNotesByAlertIDs, just returns list of Note IDs instead of list of Note objects - suspend fun getNoteIDsByAlertIDs(client: Client, alertIDs: List): List { - val notes = getNotesByAlertIDs(client, alertIDs) - return notes.map { it.id } + // Identical to getCommentsByAlertIDs, just returns list of Comment IDs instead of list of Comment objects + suspend fun getCommentIDsByAlertIDs(client: Client, alertIDs: List): List { + val comments = getCommentsByAlertIDs(client, alertIDs) + return comments.map { it.id } } - // TODO: make getNotesByAlertID and getNoteIDsByAlertID + // TODO: make getCommentsByAlertID and getCommentIDsByAlertID } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt index 74998fc9a..90d85c436 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/IndexUtils.kt @@ -9,8 +9,8 @@ import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest import org.opensearch.action.support.IndicesOptions import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.alerting.alerts.AlertIndices +import org.opensearch.alerting.comments.CommentsIndices import org.opensearch.alerting.core.ScheduledJobIndices -import org.opensearch.alerting.notes.NotesIndices import org.opensearch.client.IndicesAdminClient import org.opensearch.cluster.ClusterState import org.opensearch.cluster.metadata.IndexAbstraction @@ -36,7 +36,7 @@ class IndexUtils { private set var findingIndexSchemaVersion: Int private set - var alertingNoteIndexSchemaVersion: Int + var alertingCommentIndexSchemaVersion: Int private set var scheduledJobIndexUpdated: Boolean = false @@ -45,17 +45,17 @@ class IndexUtils { private set var findingIndexUpdated: Boolean = false private set - var notesIndexUpdated: Boolean = false + var commentsIndexUpdated: Boolean = false private set var lastUpdatedAlertHistoryIndex: String? = null var lastUpdatedFindingHistoryIndex: String? = null - var lastUpdatedNotesHistoryIndex: String? = null + var lastUpdatedCommentsHistoryIndex: String? = null init { scheduledJobIndexSchemaVersion = getSchemaVersion(ScheduledJobIndices.scheduledJobMappings()) alertIndexSchemaVersion = getSchemaVersion(AlertIndices.alertMapping()) findingIndexSchemaVersion = getSchemaVersion(AlertIndices.findingMapping()) - alertingNoteIndexSchemaVersion = getSchemaVersion(NotesIndices.notesMapping()) + alertingCommentIndexSchemaVersion = getSchemaVersion(CommentsIndices.commentsMapping()) } @JvmStatic @@ -74,8 +74,8 @@ class IndexUtils { } @JvmStatic - fun notesIndexUpdated() { - notesIndexUpdated = true + fun commentsIndexUpdated() { + commentsIndexUpdated = true } @JvmStatic diff --git a/alerting/src/main/resources/org/opensearch/alerting/notes/alerting_notes.json b/alerting/src/main/resources/org/opensearch/alerting/comments/alerting_comments.json similarity index 100% rename from alerting/src/main/resources/org/opensearch/alerting/notes/alerting_notes.json rename to alerting/src/main/resources/org/opensearch/alerting/comments/alerting_comments.json diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index abd813f59..0ffd3ba03 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -45,13 +45,13 @@ import org.opensearch.commons.alerting.action.GetFindingsResponse import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.BucketLevelTrigger import org.opensearch.commons.alerting.model.ChainedAlertTrigger +import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.alerting.model.DocLevelMonitorInput import org.opensearch.commons.alerting.model.DocLevelQuery import org.opensearch.commons.alerting.model.DocumentLevelTrigger import org.opensearch.commons.alerting.model.Finding import org.opensearch.commons.alerting.model.FindingWithDocs import org.opensearch.commons.alerting.model.Monitor -import org.opensearch.commons.alerting.model.Note import org.opensearch.commons.alerting.model.QueryLevelTrigger import org.opensearch.commons.alerting.model.ScheduledJob import org.opensearch.commons.alerting.model.SearchInput @@ -523,34 +523,36 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { return alert.copy(id = alertJson["_id"] as String, version = (alertJson["_version"] as Int).toLong()) } - protected fun createAlertNote(alertId: String, content: String): Note { + protected fun createAlertComment(alertId: String, content: String): Comment { val createRequestBody = jsonBuilder() .startObject() - .field(Note.NOTE_CONTENT_FIELD, content) + .field(Comment.COMMENT_CONTENT_FIELD, content) .endObject() .string() val createResponse = client().makeRequest( "POST", - "$ALERTING_BASE_URI/alerts/$alertId/notes", + "$ALERTING_BASE_URI/alerts/$alertId/comments", StringEntity(createRequestBody, APPLICATION_JSON) ) assertEquals("Unable to create a new alert", RestStatus.CREATED, createResponse.restStatus()) val responseBody = createResponse.asMap() - val noteId = responseBody["_id"] as String - assertNotEquals("response is missing Id", Note.NO_ID, noteId) - - val note = responseBody["note"] as Map<*, *> - - return Note( - id = noteId, - entityId = note["entity_id"] as String, - content = note["content"] as String, - createdTime = Instant.ofEpochMilli(note["created_time"] as Long), - lastUpdatedTime = if (note["last_updated_time"] != null) Instant.ofEpochMilli(note["last_updated_time"] as Long) else null, - user = note["user"]?.let { User(it as String, emptyList(), emptyList(), emptyList()) } + val commentId = responseBody["_id"] as String + assertNotEquals("response is missing Id", Comment.NO_ID, commentId) + + val comment = responseBody["comment"] as Map<*, *> + + return Comment( + id = commentId, + entityId = comment["entity_id"] as String, + content = comment["content"] as String, + createdTime = Instant.ofEpochMilli(comment["created_time"] as Long), + lastUpdatedTime = if (comment["last_updated_time"] != null) { + Instant.ofEpochMilli(comment["last_updated_time"] as Long) + } else null, + user = comment["user"]?.let { User(it as String, emptyList(), emptyList(), emptyList()) } ) } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt new file mode 100644 index 000000000..9896a9957 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt @@ -0,0 +1,153 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.resthandler + +import org.apache.hc.core5.http.ContentType +import org.apache.hc.core5.http.io.entity.StringEntity +import org.opensearch.alerting.ALERTING_BASE_URI +import org.opensearch.alerting.AlertingRestTestCase +import org.opensearch.alerting.makeRequest +import org.opensearch.alerting.randomAlert +import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_COMMENTS_ENABLED +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.alerting.model.Alert +import org.opensearch.commons.alerting.model.Comment.Companion.COMMENT_CONTENT_FIELD +import org.opensearch.commons.alerting.util.string +import org.opensearch.core.rest.RestStatus +import org.opensearch.index.query.QueryBuilders +import org.opensearch.search.builder.SearchSourceBuilder +import org.opensearch.test.OpenSearchTestCase +import org.opensearch.test.junit.annotations.TestLogging +import java.util.concurrent.TimeUnit + +@TestLogging("level:DEBUG", reason = "Debug for tests.") +@Suppress("UNCHECKED_CAST") +class AlertingCommentsRestApiIT : AlertingRestTestCase() { + + fun `test creating comment`() { + client().updateSettings(ALERTING_COMMENTS_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val commentContent = "test comment" + + val comment = createAlertComment(alertId, commentContent) + + assertEquals("Comment does not have correct content", commentContent, comment.content) + assertEquals("Comment does not have correct alert ID", alertId, comment.entityId) + } + + fun `test updating comment`() { + client().updateSettings(ALERTING_COMMENTS_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val commentContent = "test comment" + + val commentId = createAlertComment(alertId, commentContent).id + + val updateContent = "updated comment" + val updateRequestBody = XContentFactory.jsonBuilder() + .startObject() + .field(COMMENT_CONTENT_FIELD, updateContent) + .endObject() + .string() + + val updateResponse = client().makeRequest( + "PUT", + "$ALERTING_BASE_URI/alerts/comments/$commentId", + StringEntity(updateRequestBody, ContentType.APPLICATION_JSON) + ) + + assertEquals("Update comment failed", RestStatus.OK, updateResponse.restStatus()) + + val updateResponseBody = updateResponse.asMap() + + val comment = updateResponseBody["comment"] as Map<*, *> + val actualContent = comment["content"] as String + assertEquals("Comment does not have correct content after update", updateContent, actualContent) + } + + fun `test searching single comment by alert id`() { + client().updateSettings(ALERTING_COMMENTS_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val commentContent = "test comment" + + createAlertComment(alertId, commentContent) + + OpenSearchTestCase.waitUntil({ + return@waitUntil false + }, 3, TimeUnit.SECONDS) + + val search = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).toString() + val searchResponse = client().makeRequest( + "GET", + "$ALERTING_BASE_URI/alerts/comments/_search", + StringEntity(search, ContentType.APPLICATION_JSON) + ) + + val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content) + val hits = xcp.map()["hits"]!! as Map> + logger.info("hits: $hits") + val numberDocsFound = hits["total"]?.get("value") + assertEquals("No Comments found", 1, numberDocsFound) + + val searchHits = hits["hits"] as List<*> + val hit = searchHits[0] as Map<*, *> + val commentHit = hit["_source"] as Map<*, *> + assertEquals("returned Comment does not match alert id in search query", alertId, commentHit["entity_id"]) + assertEquals("returned Comment does not have expected content", commentContent, commentHit["content"]) + } + + fun `test deleting comments`() { + client().updateSettings(ALERTING_COMMENTS_ENABLED.key, "true") + + val monitor = createRandomMonitor(refresh = true) + val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + val alertId = alert.id + val commentContent = "test comment" + + val commentId = createAlertComment(alertId, commentContent).id + OpenSearchTestCase.waitUntil({ + return@waitUntil false + }, 3, TimeUnit.SECONDS) + + val deleteResponse = client().makeRequest( + "DELETE", + "$ALERTING_BASE_URI/alerts/comments/$commentId" + ) + + assertEquals("Delete comment failed", RestStatus.OK, deleteResponse.restStatus()) + + val deleteResponseBody = deleteResponse.asMap() + + val deletedCommentId = deleteResponseBody["_id"] as String + assertEquals("Deleted Comment ID does not match Comment ID in delete request", commentId, deletedCommentId) + } + + // TODO: test list + /* + create comment with empty content should fail + create without alert id should fail + update without comment id should fail + search comments across multiple alerts + (belongs in CommentsIT) create comment thats too large based on cluster setting should fail + create comment on alert that alrdy has max comments based on cluster setting should fail + create comment on alert user doesn't have backend roles to view should fail + search comment on alert user doesn't have backend roles to view should fail + comments are shown in notifications for query monitor + comments are shown in notifications for bucket monitor + (belongs in CommentsIT) update comment that user didn't author should fail + (belongs in CommentsIT) delete comment that user didn't author should fail + (belongs in CommentsIT) update comment that user didn't author but user is Admin should succeed + */ +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt deleted file mode 100644 index eb8ffc00d..000000000 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingNotesRestApiIT.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.opensearch.alerting.resthandler - -import org.apache.hc.core5.http.ContentType -import org.apache.hc.core5.http.io.entity.StringEntity -import org.opensearch.alerting.ALERTING_BASE_URI -import org.opensearch.alerting.AlertingRestTestCase -import org.opensearch.alerting.makeRequest -import org.opensearch.alerting.randomAlert -import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_NOTES_ENABLED -import org.opensearch.common.xcontent.XContentFactory -import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.alerting.model.Alert -import org.opensearch.commons.alerting.model.Note.Companion.NOTE_CONTENT_FIELD -import org.opensearch.commons.alerting.util.string -import org.opensearch.core.rest.RestStatus -import org.opensearch.index.query.QueryBuilders -import org.opensearch.search.builder.SearchSourceBuilder -import org.opensearch.test.OpenSearchTestCase -import org.opensearch.test.junit.annotations.TestLogging -import java.util.concurrent.TimeUnit - -@TestLogging("level:DEBUG", reason = "Debug for tests.") -@Suppress("UNCHECKED_CAST") -class AlertingNotesRestApiIT : AlertingRestTestCase() { - - fun `test creating note`() { - client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") - - val monitor = createRandomMonitor(refresh = true) - val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) - val alertId = alert.id - val noteContent = "test note" - - val note = createAlertNote(alertId, noteContent) - - assertEquals("Note does not have correct content", noteContent, note.content) - assertEquals("Note does not have correct alert ID", alertId, note.entityId) - } - - fun `test updating note`() { - client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") - - val monitor = createRandomMonitor(refresh = true) - val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) - val alertId = alert.id - val noteContent = "test note" - - val noteId = createAlertNote(alertId, noteContent).id - - val updateContent = "updated note" - val updateRequestBody = XContentFactory.jsonBuilder() - .startObject() - .field(NOTE_CONTENT_FIELD, updateContent) - .endObject() - .string() - - val updateResponse = client().makeRequest( - "PUT", - "$ALERTING_BASE_URI/alerts/notes/$noteId", - StringEntity(updateRequestBody, ContentType.APPLICATION_JSON) - ) - - assertEquals("Update note failed", RestStatus.OK, updateResponse.restStatus()) - - val updateResponseBody = updateResponse.asMap() - - val note = updateResponseBody["note"] as Map<*, *> - val actualContent = note["content"] as String - assertEquals("Note does not have correct content after update", updateContent, actualContent) - } - - fun `test searching single note by alert id`() { - client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") - - val monitor = createRandomMonitor(refresh = true) - val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) - val alertId = alert.id - val noteContent = "test note" - - createAlertNote(alertId, noteContent) - - OpenSearchTestCase.waitUntil({ - return@waitUntil false - }, 3, TimeUnit.SECONDS) - - val search = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).toString() - val searchResponse = client().makeRequest( - "GET", - "$ALERTING_BASE_URI/alerts/notes/_search", - StringEntity(search, ContentType.APPLICATION_JSON) - ) - - val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content) - val hits = xcp.map()["hits"]!! as Map> - logger.info("hits: $hits") - val numberDocsFound = hits["total"]?.get("value") - assertEquals("No Notes found", 1, numberDocsFound) - - val searchHits = hits["hits"] as List<*> - val hit = searchHits[0] as Map<*, *> - val noteHit = hit["_source"] as Map<*, *> - assertEquals("returned Note does not match alert id in search query", alertId, noteHit["entity_id"]) - assertEquals("returned Note does not have expected content", noteContent, noteHit["content"]) - } - - fun `test deleting notes`() { - client().updateSettings(ALERTING_NOTES_ENABLED.key, "true") - - val monitor = createRandomMonitor(refresh = true) - val alert = createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) - val alertId = alert.id - val noteContent = "test note" - - val noteId = createAlertNote(alertId, noteContent).id - OpenSearchTestCase.waitUntil({ - return@waitUntil false - }, 3, TimeUnit.SECONDS) - - val deleteResponse = client().makeRequest( - "DELETE", - "$ALERTING_BASE_URI/alerts/notes/$noteId" - ) - - assertEquals("Delete note failed", RestStatus.OK, deleteResponse.restStatus()) - - val deleteResponseBody = deleteResponse.asMap() - - val deletedNoteId = deleteResponseBody["_id"] as String - assertEquals("Deleted Note ID does not match Note ID in delete request", noteId, deletedNoteId) - } - - // TODO: test list - /* - create note with empty content should fail - create without alert id should fail - update without note id should fail - search notes across multiple alerts - (belongs in NotesIT) create note thats too large based on cluster setting should fail - create note on alert that alrdy has max notes based on cluster setting should fail - create note on alert user doesn't have backend roles to view should fail - search note on alert user doesn't have backend roles to view should fail - notes are shown in notifications for query monitor - notes are shown in notifications for bucket monitor - (belongs in NotesIT) update note that user didn't author should fail - (belongs in NotesIT) delete note that user didn't author should fail - (belongs in NotesIT) update note that user didn't author but user is Admin should succeed - */ -} From 889ecf22f8f1c3384b0e1d3382b91bea73e9a465 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 6 Jun 2024 12:22:48 -0700 Subject: [PATCH 08/22] misc changes Signed-off-by: Dennis Toepker --- .../main/kotlin/org/opensearch/alerting/AlertingPlugin.kt | 1 + .../org/opensearch/alerting/QueryLevelMonitorRunner.kt | 2 -- .../kotlin/org/opensearch/alerting/alerts/AlertIndices.kt | 1 - .../resthandler/RestDeleteAlertingCommentAction.kt | 4 ++-- .../resthandler/RestIndexAlertingCommentAction.kt | 8 ++++---- .../resthandler/RestSearchAlertingCommentAction.kt | 4 ++-- .../alerting/script/BucketLevelTriggerExecutionContext.kt | 2 -- .../alerting/script/QueryLevelTriggerExecutionContext.kt | 2 -- .../transport/TransportDeleteAlertingCommentAction.kt | 2 +- .../transport/TransportIndexAlertingCommentAction.kt | 2 +- .../transport/TransportSearchAlertingCommentAction.kt | 2 +- .../org/opensearch/alerting/AlertingRestTestCase.kt | 3 ++- .../alerting/resthandler/AlertingCommentsRestApiIT.kt | 8 ++++---- 13 files changed, 18 insertions(+), 23 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index 90eeb2852..d2dc22317 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -160,6 +160,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R @JvmField val LEGACY_OPENDISTRO_EMAIL_ACCOUNT_BASE_URI = "$LEGACY_OPENDISTRO_DESTINATION_BASE_URI/email_accounts" @JvmField val LEGACY_OPENDISTRO_EMAIL_GROUP_BASE_URI = "$LEGACY_OPENDISTRO_DESTINATION_BASE_URI/email_groups" @JvmField val FINDING_BASE_URI = "/_plugins/_alerting/findings" + @JvmField val COMMENTS_BASE_URI = "/_plugins/_alerting/comments" @JvmField val ALERTING_JOB_TYPES = listOf("monitor", "workflow") } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index 3067a872d..879417b63 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -7,8 +7,6 @@ package org.opensearch.alerting import org.apache.logging.log4j.LogManager import org.opensearch.alerting.model.AlertContext -import org.opensearch.alerting.model.MonitorRunResult -import org.opensearch.alerting.model.QueryLevelTriggerRunResult import org.opensearch.alerting.opensearchapi.InjectorContextElement import org.opensearch.alerting.opensearchapi.withClosableContext import org.opensearch.alerting.script.QueryLevelTriggerExecutionContext diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index 51a3eb0b3..b858d1a51 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -41,7 +41,6 @@ import org.opensearch.alerting.settings.AlertingSettings.Companion.FINDING_HISTO import org.opensearch.alerting.settings.AlertingSettings.Companion.FINDING_HISTORY_RETENTION_PERIOD import org.opensearch.alerting.settings.AlertingSettings.Companion.FINDING_HISTORY_ROLLOVER_PERIOD import org.opensearch.alerting.settings.AlertingSettings.Companion.REQUEST_TIMEOUT -import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.CommentsUtils import org.opensearch.alerting.util.IndexUtils import org.opensearch.client.Client diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt index 1a33888ea..b19c6610f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt @@ -32,14 +32,14 @@ class RestDeleteAlertingCommentAction : BaseRestHandler() { return listOf( Route( RestRequest.Method.DELETE, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/{commentID}" + "${AlertingPlugin.COMMENTS_BASE_URI}/{commentID}" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/{commentID}") + log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/{commentID}") val commentId = request.param("commentID") val deleteMonitorRequest = DeleteCommentRequest(commentId) return RestChannelConsumer { channel -> diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index 1f6476b0e..ea4ed1023 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -7,7 +7,6 @@ package org.opensearch.alerting.resthandler import org.apache.logging.log4j.LogManager import org.opensearch.alerting.AlertingPlugin -import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.IF_PRIMARY_TERM import org.opensearch.alerting.util.IF_SEQ_NO import org.opensearch.client.node.NodeClient @@ -16,6 +15,7 @@ import org.opensearch.commons.alerting.action.IndexCommentRequest import org.opensearch.commons.alerting.action.IndexCommentResponse import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Comment +import org.opensearch.commons.alerting.util.AlertingException import org.opensearch.core.rest.RestStatus import org.opensearch.core.xcontent.ToXContent import org.opensearch.index.seqno.SequenceNumbers @@ -43,18 +43,18 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { return listOf( Route( RestRequest.Method.POST, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/{alertID}/comments" + "${AlertingPlugin.COMMENTS_BASE_URI}/{alertID}" ), Route( RestRequest.Method.PUT, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/{commentID}" + "${AlertingPlugin.COMMENTS_BASE_URI}/{commentID}" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments") + log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}") val alertId = request.param("alertID", Alert.NO_ID) val commentId = request.param("commentID", Comment.NO_ID) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt index ef073df7b..c1a590f4c 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt @@ -46,14 +46,14 @@ class RestSearchAlertingCommentAction() : BaseRestHandler() { return listOf( Route( RestRequest.Method.GET, - "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/_search" + "${AlertingPlugin.COMMENTS_BASE_URI}/_search" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/_search") + log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/_search") val searchSourceBuilder = SearchSourceBuilder() searchSourceBuilder.parseXContent(request.contentOrSourceParamParser()) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt index 51c1fa592..fe6e382f8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/script/BucketLevelTriggerExecutionContext.kt @@ -7,8 +7,6 @@ package org.opensearch.alerting.script import org.apache.logging.log4j.LogManager import org.opensearch.alerting.model.AlertContext -import org.opensearch.alerting.model.BucketLevelTriggerRunResult -import org.opensearch.alerting.model.MonitorRunResult import org.opensearch.commons.alerting.model.BucketLevelTrigger import org.opensearch.commons.alerting.model.BucketLevelTriggerRunResult import org.opensearch.commons.alerting.model.Monitor diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt index c2c5cacd6..6ecde6d6e 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/script/QueryLevelTriggerExecutionContext.kt @@ -6,8 +6,6 @@ package org.opensearch.alerting.script import org.opensearch.alerting.model.AlertContext -import org.opensearch.alerting.model.MonitorRunResult -import org.opensearch.alerting.model.QueryLevelTriggerRunResult import org.opensearch.commons.alerting.model.Monitor import org.opensearch.commons.alerting.model.MonitorRunResult import org.opensearch.commons.alerting.model.QueryLevelTrigger diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index 94b2a52a3..8462a88a0 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -19,7 +19,6 @@ import org.opensearch.action.support.HandledTransportAction import org.opensearch.alerting.comments.CommentsIndices.Companion.ALL_COMMENTS_INDEX_PATTERN import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.settings.AlertingSettings -import org.opensearch.alerting.util.AlertingException import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -31,6 +30,7 @@ import org.opensearch.commons.alerting.action.AlertingActions import org.opensearch.commons.alerting.action.DeleteCommentRequest import org.opensearch.commons.alerting.action.DeleteCommentResponse import org.opensearch.commons.alerting.model.Comment +import org.opensearch.commons.alerting.util.AlertingException import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index a22c255b7..ee8200f9f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -28,7 +28,6 @@ import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_COMM import org.opensearch.alerting.settings.AlertingSettings.Companion.COMMENTS_MAX_CONTENT_SIZE import org.opensearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT import org.opensearch.alerting.settings.AlertingSettings.Companion.MAX_COMMENTS_PER_ALERT -import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.CommentsUtils import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService @@ -43,6 +42,7 @@ import org.opensearch.commons.alerting.action.IndexCommentRequest import org.opensearch.commons.alerting.action.IndexCommentResponse import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Comment +import org.opensearch.commons.alerting.util.AlertingException import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt index 549725c40..f78343b6b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt @@ -18,7 +18,6 @@ import org.opensearch.action.support.HandledTransportAction import org.opensearch.alerting.alerts.AlertIndices.Companion.ALL_ALERT_INDEX_PATTERN import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.settings.AlertingSettings -import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.use import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService @@ -31,6 +30,7 @@ import org.opensearch.commons.alerting.action.AlertingActions import org.opensearch.commons.alerting.action.SearchCommentRequest import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Comment +import org.opensearch.commons.alerting.util.AlertingException import org.opensearch.commons.authuser.User import org.opensearch.commons.utils.recreateObject import org.opensearch.core.action.ActionListener diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index 0ffd3ba03..45cc34474 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -14,6 +14,7 @@ import org.apache.hc.core5.http.message.BasicHeader import org.junit.AfterClass import org.junit.rules.DisableOnDebug import org.opensearch.action.search.SearchResponse +import org.opensearch.alerting.AlertingPlugin.Companion.COMMENTS_BASE_URI import org.opensearch.alerting.AlertingPlugin.Companion.EMAIL_ACCOUNT_BASE_URI import org.opensearch.alerting.AlertingPlugin.Companion.EMAIL_GROUP_BASE_URI import org.opensearch.alerting.alerts.AlertIndices @@ -532,7 +533,7 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { val createResponse = client().makeRequest( "POST", - "$ALERTING_BASE_URI/alerts/$alertId/comments", + "$COMMENTS_BASE_URI/$alertId", StringEntity(createRequestBody, APPLICATION_JSON) ) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt index 9896a9957..601a1860c 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/AlertingCommentsRestApiIT.kt @@ -7,7 +7,7 @@ package org.opensearch.alerting.resthandler import org.apache.hc.core5.http.ContentType import org.apache.hc.core5.http.io.entity.StringEntity -import org.opensearch.alerting.ALERTING_BASE_URI +import org.opensearch.alerting.AlertingPlugin.Companion.COMMENTS_BASE_URI import org.opensearch.alerting.AlertingRestTestCase import org.opensearch.alerting.makeRequest import org.opensearch.alerting.randomAlert @@ -61,7 +61,7 @@ class AlertingCommentsRestApiIT : AlertingRestTestCase() { val updateResponse = client().makeRequest( "PUT", - "$ALERTING_BASE_URI/alerts/comments/$commentId", + "$COMMENTS_BASE_URI/$commentId", StringEntity(updateRequestBody, ContentType.APPLICATION_JSON) ) @@ -91,7 +91,7 @@ class AlertingCommentsRestApiIT : AlertingRestTestCase() { val search = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).toString() val searchResponse = client().makeRequest( "GET", - "$ALERTING_BASE_URI/alerts/comments/_search", + "$COMMENTS_BASE_URI/_search", StringEntity(search, ContentType.APPLICATION_JSON) ) @@ -123,7 +123,7 @@ class AlertingCommentsRestApiIT : AlertingRestTestCase() { val deleteResponse = client().makeRequest( "DELETE", - "$ALERTING_BASE_URI/alerts/comments/$commentId" + "$COMMENTS_BASE_URI/$commentId" ) assertEquals("Delete comment failed", RestStatus.OK, deleteResponse.restStatus()) From c141956f902a6d3115f15bd68b4033aaafd57a18 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 6 Jun 2024 12:36:11 -0700 Subject: [PATCH 09/22] changed API endpoints Signed-off-by: Dennis Toepker --- .../RestDeleteAlertingCommentAction.kt | 6 +++--- .../RestIndexAlertingCommentAction.kt | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt index b19c6610f..c73393819 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt @@ -32,15 +32,15 @@ class RestDeleteAlertingCommentAction : BaseRestHandler() { return listOf( Route( RestRequest.Method.DELETE, - "${AlertingPlugin.COMMENTS_BASE_URI}/{commentID}" + "${AlertingPlugin.COMMENTS_BASE_URI}/{id}" ) ) } @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/{commentID}") - val commentId = request.param("commentID") + log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/{id}") + val commentId = request.param("id") val deleteMonitorRequest = DeleteCommentRequest(commentId) return RestChannelConsumer { channel -> client.execute(AlertingActions.DELETE_COMMENT_ACTION_TYPE, deleteMonitorRequest, RestToXContentListener(channel)) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index ea4ed1023..7cdcd0077 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -43,11 +43,11 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { return listOf( Route( RestRequest.Method.POST, - "${AlertingPlugin.COMMENTS_BASE_URI}/{alertID}" + "${AlertingPlugin.COMMENTS_BASE_URI}/{id}" ), Route( RestRequest.Method.PUT, - "${AlertingPlugin.COMMENTS_BASE_URI}/{commentID}" + "${AlertingPlugin.COMMENTS_BASE_URI}/{id}" ) ) } @@ -56,14 +56,19 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}") - val alertId = request.param("alertID", Alert.NO_ID) - val commentId = request.param("commentID", Comment.NO_ID) - if (request.method() == RestRequest.Method.POST && Alert.NO_ID == alertId) { + val id = request.param( + "id", + if (request.method() == RestRequest.Method.POST) Alert.NO_ID else Comment.NO_ID + ) + if (request.method() == RestRequest.Method.POST && Alert.NO_ID == id) { throw AlertingException.wrap(IllegalArgumentException("Missing alert ID")) - } else if (request.method() == RestRequest.Method.PUT && Comment.NO_ID == commentId) { + } else if (request.method() == RestRequest.Method.PUT && Comment.NO_ID == id) { throw AlertingException.wrap(IllegalArgumentException("Missing comment ID")) } + val alertId = if (request.method() == RestRequest.Method.POST) id else Alert.NO_ID + val commentId = if (request.method() == RestRequest.Method.PUT) id else Comment.NO_ID + val content = request.contentParser().map()["content"] as String? if (content.isNullOrEmpty()) { throw AlertingException.wrap(IllegalArgumentException("Missing comment content")) From 97b75a698ef3899792efb1c7838251dea3c4c28b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 6 Jun 2024 15:54:52 -0700 Subject: [PATCH 10/22] more misc changes and fixes Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 14 ------------- .../alerting/BucketLevelMonitorRunner.kt | 21 +++++++++++++++---- .../alerting/QueryLevelMonitorRunner.kt | 3 ++- .../opensearch/alerting/model/AlertContext.kt | 2 +- .../RestSearchAlertingCommentAction.kt | 4 ++++ .../opensearch/alerting/util/CommentsUtils.kt | 15 ++++++++++++- .../alerting/comments/alerting_comments.json | 8 +++++-- 7 files changed, 44 insertions(+), 23 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index dfa097643..cc0a2c8bd 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -42,7 +42,6 @@ import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.BucketLevelTrigger import org.opensearch.commons.alerting.model.ChainedAlertTriggerRunResult import org.opensearch.commons.alerting.model.ClusterMetricsTriggerRunResult -import org.opensearch.commons.alerting.model.Comment import org.opensearch.commons.alerting.model.DataSources import org.opensearch.commons.alerting.model.Monitor import org.opensearch.commons.alerting.model.NoOpTrigger @@ -914,17 +913,4 @@ class AlertService( else -> throw IllegalStateException("Unreachable code reached!") } } - - /** - * Performs a Search request to retrieve the top maxComments most recent comments associated with the - * given Alert, where maxComments is a cluster setting. - */ - suspend fun getCommentsForAlertNotification(alertId: String, maxComments: Int): List { - val allcomments = CommentsUtils.getCommentsByAlertIDs(client, listOf(alertId)) - val sortedcomments = allcomments.sortedByDescending { it.createdTime } - if (sortedcomments.size <= maxComments) { - return sortedcomments - } - return sortedcomments.slice(0 until maxComments) - } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index bd649db31..179958745 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -20,6 +20,7 @@ import org.opensearch.alerting.opensearchapi.suspendUntil import org.opensearch.alerting.opensearchapi.withClosableContext import org.opensearch.alerting.script.BucketLevelTriggerExecutionContext import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.util.CommentsUtils import org.opensearch.alerting.util.defaultToPerExecutionAction import org.opensearch.alerting.util.getActionExecutionPolicy import org.opensearch.alerting.util.getBucketKeysHash @@ -296,7 +297,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { for (alertCategory in actionExecutionScope.actionableAlerts) { val alertsToExecuteActionsFor = nextAlerts[trigger.id]?.get(alertCategory) ?: mutableListOf() for (alert in alertsToExecuteActionsFor) { - val alertComments = monitorCtx.alertService!!.getCommentsForAlertNotification(alert.id, maxComments) + val alertComments = CommentsUtils.getCommentsForAlertNotification(monitorCtx.client!!, alert.id, maxComments) val alertContext = if (alertCategory != AlertCategory.NEW) { AlertContext(alert = alert, comments = alertComments.ifEmpty { null }) } else { @@ -335,11 +336,19 @@ object BucketLevelMonitorRunner : MonitorRunner() { val actionCtx = triggerCtx.copy( dedupedAlerts = dedupedAlerts.map { - val dedupedAlertsComments = monitorCtx.alertService!!.getCommentsForAlertNotification(it.id, maxComments) + val dedupedAlertsComments = CommentsUtils.getCommentsForAlertNotification( + monitorCtx.client!!, + it.id, + maxComments + ) AlertContext(alert = it, comments = dedupedAlertsComments.ifEmpty { null }) }, newAlerts = newAlerts.map { - val newAlertsComments = monitorCtx.alertService!!.getCommentsForAlertNotification(it.id, maxComments) + val newAlertsComments = CommentsUtils.getCommentsForAlertNotification( + monitorCtx.client!!, + it.id, + maxComments + ) getAlertContext( alert = it, alertSampleDocs = alertSampleDocs, @@ -347,7 +356,11 @@ object BucketLevelMonitorRunner : MonitorRunner() { ) }, completedAlerts = completedAlerts.map { - val completedAlertsComments = monitorCtx.alertService!!.getCommentsForAlertNotification(it.id, maxComments) + val completedAlertsComments = CommentsUtils.getCommentsForAlertNotification( + monitorCtx.client!!, + it.id, + maxComments + ) AlertContext(alert = it, comments = completedAlertsComments.ifEmpty { null }) }, error = monitorResult.error ?: triggerResult.error diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index 879417b63..f759021f4 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -11,6 +11,7 @@ import org.opensearch.alerting.opensearchapi.InjectorContextElement import org.opensearch.alerting.opensearchapi.withClosableContext import org.opensearch.alerting.script.QueryLevelTriggerExecutionContext import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.util.CommentsUtils import org.opensearch.alerting.util.isADMonitor import org.opensearch.commons.alerting.model.Alert import org.opensearch.commons.alerting.model.Monitor @@ -71,7 +72,7 @@ object QueryLevelMonitorRunner : MonitorRunner() { val currentAlert = currentAlerts[trigger] val currentAlertContext = currentAlert?.let { val maxComments = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION) - val currentAlertComments = monitorCtx.alertService!!.getCommentsForAlertNotification(currentAlert.id, maxComments) + val currentAlertComments = CommentsUtils.getCommentsForAlertNotification(monitorCtx.client!!, currentAlert.id, maxComments) AlertContext(alert = currentAlert, comments = currentAlertComments.ifEmpty { null }) } val triggerCtx = QueryLevelTriggerExecutionContext(monitor, trigger as QueryLevelTrigger, monitorResult, currentAlertContext) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt index d52eecda4..12fa1e0e0 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/model/AlertContext.kt @@ -33,7 +33,7 @@ data class AlertContext( Comment.COMMENT_CREATED_TIME_FIELD to it.createdTime, Comment.COMMENT_LAST_UPDATED_TIME_FIELD to it.lastUpdatedTime, Comment.COMMENT_CONTENT_FIELD to it.content, - Comment.COMMENT_USER_FIELD to it.user + Comment.COMMENT_USER_FIELD to it.user?.name ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt index c1a590f4c..24ba6e9af 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt @@ -47,6 +47,10 @@ class RestSearchAlertingCommentAction() : BaseRestHandler() { Route( RestRequest.Method.GET, "${AlertingPlugin.COMMENTS_BASE_URI}/_search" + ), + Route( + RestRequest.Method.POST, + "${AlertingPlugin.COMMENTS_BASE_URI}/_search" ) ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt index ef5ccce59..d64846f37 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt @@ -56,7 +56,7 @@ class CommentsUtils { // with the Entities given by the list of Entity IDs // TODO: change this to EntityIDs suspend fun getCommentsByAlertIDs(client: Client, alertIDs: List): List { - val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("alert_id", alertIDs)) + val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("entity_id", alertIDs)) val searchSourceBuilder = SearchSourceBuilder() .version(true) @@ -90,6 +90,19 @@ class CommentsUtils { return comments.map { it.id } } + /** + * Performs a Search request to retrieve the top maxComments most recent comments associated with the + * given Alert, where maxComments is a cluster setting. + */ + suspend fun getCommentsForAlertNotification(client: Client, alertId: String, maxComments: Int): List { + val allComments = CommentsUtils.getCommentsByAlertIDs(client, listOf(alertId)) + val sortedComments = allComments.sortedByDescending { it.createdTime } + if (sortedComments.size <= maxComments) { + return sortedComments + } + return sortedComments.slice(0 until maxComments) + } + // TODO: make getCommentsByAlertID and getCommentIDsByAlertID } } diff --git a/alerting/src/main/resources/org/opensearch/alerting/comments/alerting_comments.json b/alerting/src/main/resources/org/opensearch/alerting/comments/alerting_comments.json index 0ba5a305a..967b6dfd2 100644 --- a/alerting/src/main/resources/org/opensearch/alerting/comments/alerting_comments.json +++ b/alerting/src/main/resources/org/opensearch/alerting/comments/alerting_comments.json @@ -1,13 +1,17 @@ { "dynamic": "false", "properties": { - "alert_id": { + "entity_id": { "type": "keyword" }, "content": { "type": "text" }, - "time": { + "created_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "last_updated_time": { "type": "date", "format": "strict_date_time||epoch_millis" }, From 19669dcb41eb20468a71658d1accf6cc31aa5027 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 6 Jun 2024 16:14:26 -0700 Subject: [PATCH 11/22] misc cleanup Signed-off-by: Dennis Toepker --- .../src/main/kotlin/org/opensearch/alerting/AlertService.kt | 6 +++++- .../org/opensearch/alerting/BucketLevelMonitorRunner.kt | 2 +- .../kotlin/org/opensearch/alerting/util/CommentsUtils.kt | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index cc0a2c8bd..16a2f1048 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -741,7 +741,11 @@ class AlertService( .id(alert.id) } else { // Otherwise, prepare the Alert's comments for deletion, and don't include - // a request to index the Alert to an Alert history index + // a request to index the Alert to an Alert history index. + // The delete request can't be added to the list of DocWriteRequests because + // Comments are stored in aliased history indices, not a concrete Comments + // index like Alerts. A DeleteBy request will be used to delete Comments, instead + // of a regular Delete request commentsToDeleteIDs.addAll(CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id))) null } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index 179958745..92f7b5634 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -195,7 +195,7 @@ object BucketLevelMonitorRunner : MonitorRunner() { * The new Alerts have to be returned and saved back with their indexed doc ID to prevent duplicate documents * when the Alerts are updated again after Action execution. * - * Comment: Index operations can fail for various reasons (such as write blocks on cluster), in such a case, the Actions + * Note: Index operations can fail for various reasons (such as write blocks on cluster), in such a case, the Actions * will still execute with the Alert information in the ctx but the Alerts may not be visible. */ if (!dryrun && monitor.id != Monitor.NO_ID) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt index d64846f37..8b82713eb 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt @@ -54,7 +54,6 @@ class CommentsUtils { // Searches through all Comments history indices and returns a list of all Comments associated // with the Entities given by the list of Entity IDs - // TODO: change this to EntityIDs suspend fun getCommentsByAlertIDs(client: Client, alertIDs: List): List { val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("entity_id", alertIDs)) val searchSourceBuilder = From 656fc227bdf7efa4655322031cd8fb4057d9f9b0 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Thu, 6 Jun 2024 16:30:55 -0700 Subject: [PATCH 12/22] updated a comment Signed-off-by: Dennis Toepker --- .../opensearch/alerting/comments/CommentsIndices.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt index be92e5028..38050e62a 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt @@ -198,10 +198,13 @@ class CommentsIndices( ) } - // TODO: Everything below here are util functions straight from AlertIndices.kt - // TODO: might need to reuse their code or refactor - // TODO: may merge into AlertIndices.kt if we decide to make comments indices - // TODO: component-specific instead of universal and component-agnostic + // TODO: Everything below is boilerplate util functions straight from AlertIndices.kt + /* + Depending on whether comments system indices will be component-specific or + component-agnostic, may need to either merge CommentsIndices.kt into AlertIndices.kt, + or factor these out into IndexUtils.kt for both AlertIndices.kt and CommentsIndices.kt + to use + */ private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { val indicesToDelete = mutableListOf() From 3e3a4ed40def955ee9943f8dc86100b01149f724 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 7 Jun 2024 14:23:08 -0700 Subject: [PATCH 13/22] misc cleanup and setting refresh policy to immediate Signed-off-by: Dennis Toepker --- .../RestIndexAlertingCommentAction.kt | 2 +- .../TransportDeleteAlertingCommentAction.kt | 2 + .../TransportIndexAlertingCommentAction.kt | 5 +++ .../TransportSearchAlertingCommentAction.kt | 1 + .../opensearch/alerting/util/CommentsUtils.kt | 42 +++++++++---------- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index 7cdcd0077..2a13ed800 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -94,7 +94,7 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { val restResponse = BytesRestResponse(returnStatus, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)) if (returnStatus == RestStatus.CREATED) { - val location = "${AlertingPlugin.MONITOR_BASE_URI}/alerts/comments/${response.id}" + val location = "${AlertingPlugin.COMMENTS_BASE_URI}/${response.id}" restResponse.addHeader("Location", location) } return restResponse diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index 8462a88a0..9396a0b9e 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -105,6 +105,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( ) { private var sourceIndex: String? = null + suspend fun resolveUserAndStart() { try { val comment = getComment() @@ -121,6 +122,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( // if user is null because security plugin is not installed, anyone can delete any comment // otherwise, only allow comment deletion if the deletion requester is the same as the comment's author + // or if the user is Admin val canDelete = user == null || user.name == comment.user?.name || isAdmin(user) val deleteRequest = DeleteRequest(sourceIndex, commentId) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index ee8200f9f..76dc59571 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -58,6 +58,7 @@ import org.opensearch.tasks.Task import org.opensearch.transport.TransportService import java.lang.IllegalArgumentException import java.time.Instant +import org.opensearch.action.support.WriteRequest private val log = LogManager.getLogger(TransportIndexMonitorAction::class.java) private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) @@ -218,6 +219,8 @@ constructor( .setIfSeqNo(request.seqNo) .setIfPrimaryTerm(request.primaryTerm) .timeout(indexTimeout) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) +// .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) log.info("Creating new comment: ${comment.toXContentWithUser(XContentFactory.jsonBuilder())}") @@ -294,6 +297,8 @@ constructor( .setIfSeqNo(request.seqNo) .setIfPrimaryTerm(request.primaryTerm) .timeout(indexTimeout) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) +// .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) log.info( "Updating comment, ${currentComment.id}, from: " + diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt index f78343b6b..60770b874 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt @@ -163,6 +163,7 @@ class TransportSearchAlertingCommentAction @Inject constructor( val searchRequest = SearchRequest() .source(searchSourceBuilder) .indices(ALL_ALERT_INDEX_PATTERN) + // .preference(Preference.PRIMARY_FIRST.type()) // expensive, be careful val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } val alertIDs = searchResponse.hits.map { hit -> diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt index 8b82713eb..1a2b26103 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt @@ -32,26 +32,6 @@ private val log = LogManager.getLogger(CommentsUtils::class.java) class CommentsUtils { companion object { - // Deletes all Comments given by the list of Comments IDs - suspend fun deleteComments(client: Client, commentIDs: List) { - if (commentIDs.isEmpty()) return - val deleteResponse: BulkByScrollResponse = suspendCoroutine { cont -> - DeleteByQueryRequestBuilder(client, DeleteByQueryAction.INSTANCE) - .source(ALL_COMMENTS_INDEX_PATTERN) - .filter(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", commentIDs))) - .refresh(true) - .execute( - object : ActionListener { - override fun onResponse(response: BulkByScrollResponse) = cont.resume(response) - override fun onFailure(t: Exception) = cont.resumeWithException(t) - } - ) - } - deleteResponse.bulkFailures.forEach { - log.error("Failed to delete Comment. Comment ID: [${it.id}] cause: [${it.cause}] ") - } - } - // Searches through all Comments history indices and returns a list of all Comments associated // with the Entities given by the list of Entity IDs suspend fun getCommentsByAlertIDs(client: Client, alertIDs: List): List { @@ -94,7 +74,7 @@ class CommentsUtils { * given Alert, where maxComments is a cluster setting. */ suspend fun getCommentsForAlertNotification(client: Client, alertId: String, maxComments: Int): List { - val allComments = CommentsUtils.getCommentsByAlertIDs(client, listOf(alertId)) + val allComments = getCommentsByAlertIDs(client, listOf(alertId)) val sortedComments = allComments.sortedByDescending { it.createdTime } if (sortedComments.size <= maxComments) { return sortedComments @@ -102,6 +82,26 @@ class CommentsUtils { return sortedComments.slice(0 until maxComments) } + // Deletes all Comments given by the list of Comments IDs + suspend fun deleteComments(client: Client, commentIDs: List) { + if (commentIDs.isEmpty()) return + val deleteResponse: BulkByScrollResponse = suspendCoroutine { cont -> + DeleteByQueryRequestBuilder(client, DeleteByQueryAction.INSTANCE) + .source(ALL_COMMENTS_INDEX_PATTERN) + .filter(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", commentIDs))) + .refresh(true) + .execute( + object : ActionListener { + override fun onResponse(response: BulkByScrollResponse) = cont.resume(response) + override fun onFailure(t: Exception) = cont.resumeWithException(t) + } + ) + } + deleteResponse.bulkFailures.forEach { + log.error("Failed to delete Comment. Comment ID: [${it.id}] cause: [${it.cause}] ") + } + } + // TODO: make getCommentsByAlertID and getCommentIDsByAlertID } } From a08756b7cc51f44bb8fa6e0e304f45e19220fb20 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Fri, 7 Jun 2024 14:48:46 -0700 Subject: [PATCH 14/22] fixed lint issues and other restructuring Signed-off-by: Dennis Toepker --- .../TransportDeleteAlertingCommentAction.kt | 2 +- .../TransportIndexAlertingCommentAction.kt | 131 ++++++++++-------- .../TransportSearchAlertingCommentAction.kt | 2 +- 3 files changed, 72 insertions(+), 63 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index 9396a0b9e..2b9a5913a 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -159,7 +159,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( if (searchResponse.hits.totalHits.value == 0L) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Comment with $commentId is not found", RestStatus.NOT_FOUND) + OpenSearchStatusException("Comment with ID $commentId is not found", RestStatus.NOT_FOUND) ) ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index 76dc59571..2a75c739e 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -19,6 +19,7 @@ import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction +import org.opensearch.action.support.WriteRequest import org.opensearch.alerting.alerts.AlertIndices import org.opensearch.alerting.comments.CommentsIndices import org.opensearch.alerting.comments.CommentsIndices.Companion.COMMENTS_HISTORY_WRITE_INDEX @@ -58,7 +59,6 @@ import org.opensearch.tasks.Task import org.opensearch.transport.TransportService import java.lang.IllegalArgumentException import java.time.Instant -import org.opensearch.action.support.WriteRequest private val log = LogManager.getLogger(TransportIndexMonitorAction::class.java) private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) @@ -153,38 +153,8 @@ constructor( } private suspend fun indexComment() { - // need to validate the existence of the Alert that user is trying to add Comment to. - // Also need to check if user has permissions to add a Comment to the passed in Alert. To do this, - // we retrieve the Alert to get its associated monitor user, and use that to - // check if they have permissions to the Monitor that generated the Alert - val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.entityId))) - val searchSourceBuilder = - SearchSourceBuilder() - .version(true) - .seqNoAndPrimaryTerm(true) - .query(queryBuilder) - - // search all alerts, since user might want to create a comment - // on a completed alert - val searchRequest = - SearchRequest() - .indices(AlertIndices.ALL_ALERT_INDEX_PATTERN) - .source(searchSourceBuilder) - - val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } - val alerts = searchResponse.hits.map { hit -> - val xcp = XContentHelper.createParser( - NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, - hit.sourceRef, - XContentType.JSON - ) - XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) - val alert = Alert.parse(xcp, hit.id, hit.version) - alert - } - - if (alerts.isEmpty()) { + val alert = getAlert() + if (alert == null) { actionListener.onFailure( AlertingException.wrap( OpenSearchStatusException("Alert with ID ${request.entityId} is not found", RestStatus.NOT_FOUND), @@ -193,8 +163,6 @@ constructor( return } - val alert = alerts[0] // there should only be 1 Alert that matched the request alert ID - val numCommentsOnThisAlert = CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id)).size if (numCommentsOnThisAlert >= maxCommentsPerAlert) { actionListener.onFailure( @@ -243,34 +211,16 @@ constructor( } private suspend fun updateComment() { - val getRequest = GetRequest(COMMENTS_HISTORY_WRITE_INDEX, request.commentId) - try { - val getResponse: GetResponse = client.suspendUntil { client.get(getRequest, it) } - if (!getResponse.isExists) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException("Comment with ${request.commentId} is not found", RestStatus.NOT_FOUND), - ), - ) - return - } - val xcp = - XContentHelper.createParser( - xContentRegistry, - LoggingDeprecationHandler.INSTANCE, - getResponse.sourceAsBytesRef, - XContentType.JSON, - ) - xcp.nextToken() - val comment = Comment.parse(xcp, getResponse.id) - log.info("comment to be updated: $comment") - onGetCommentResponse(comment) - } catch (t: Exception) { - actionListener.onFailure(AlertingException.wrap(t)) + val currentComment = getComment() + if (currentComment == null) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Comment with ID ${request.commentId} is not found", RestStatus.NOT_FOUND), + ), + ) + return } - } - private suspend fun onGetCommentResponse(currentComment: Comment) { // check that the user has permissions to edit the comment. user can edit comment if // - user is Admin // - user is the author of the comment @@ -329,6 +279,65 @@ constructor( } } + private suspend fun getAlert(): Alert? { + // need to validate the existence of the Alert that user is trying to add Comment to. + // Also need to check if user has permissions to add a Comment to the passed in Alert. To do this, + // we retrieve the Alert to get its associated monitor user, and use that to + // check if they have permissions to the Monitor that generated the Alert + val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.entityId))) + val searchSourceBuilder = + SearchSourceBuilder() + .version(true) + .seqNoAndPrimaryTerm(true) + .query(queryBuilder) + + // search all alerts, since user might want to create a comment + // on a completed alert + val searchRequest = + SearchRequest() + .indices(AlertIndices.ALL_ALERT_INDEX_PATTERN) + .source(searchSourceBuilder) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + val alerts = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val alert = Alert.parse(xcp, hit.id, hit.version) + alert + } + + if (alerts.isEmpty()) return null + + return alerts[0] // there should only be 1 Alert that matched the request alert ID + } + + private suspend fun getComment(): Comment? { + val getRequest = GetRequest(COMMENTS_HISTORY_WRITE_INDEX, request.commentId) + try { + val getResponse: GetResponse = client.suspendUntil { client.get(getRequest, it) } + if (!getResponse.isExists) return null + val xcp = + XContentHelper.createParser( + xContentRegistry, + LoggingDeprecationHandler.INSTANCE, + getResponse.sourceAsBytesRef, + XContentType.JSON, + ) + xcp.nextToken() + val comment = Comment.parse(xcp, getResponse.id) + log.info("comment to be updated: $comment") + return comment + } catch (t: Exception) { + actionListener.onFailure(AlertingException.wrap(t)) + return null + } + } + private fun checkShardsFailure(response: IndexResponse): String? { val failureReasons = StringBuilder() if (response.shardInfo.failed > 0) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt index 60770b874..525aa25a7 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt @@ -163,7 +163,7 @@ class TransportSearchAlertingCommentAction @Inject constructor( val searchRequest = SearchRequest() .source(searchSourceBuilder) .indices(ALL_ALERT_INDEX_PATTERN) - // .preference(Preference.PRIMARY_FIRST.type()) // expensive, be careful + // .preference(Preference.PRIMARY_FIRST.type()) // expensive, be careful val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } val alertIDs = searchResponse.hits.map { hit -> From 49e5bfc06f2f43c84c5c3dedffdbd2867e595260 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 10 Jun 2024 10:23:54 -0700 Subject: [PATCH 15/22] review-based changes Signed-off-by: Dennis Toepker --- .../org/opensearch/alerting/AlertService.kt | 6 +- .../alerting/BucketLevelMonitorRunner.kt | 40 +++++----- .../org/opensearch/alerting/MonitorRunner.kt | 2 + .../alerting/QueryLevelMonitorRunner.kt | 12 ++- .../alerting/alerts/AlertIndices.kt | 4 +- .../alerting/comments/CommentsIndices.kt | 8 +- .../RestDeleteAlertingCommentAction.kt | 2 +- .../RestIndexAlertingCommentAction.kt | 4 +- .../RestSearchAlertingCommentAction.kt | 2 +- .../resthandler/RestSearchMonitorAction.kt | 2 +- .../TransportDeleteAlertingCommentAction.kt | 2 +- .../TransportIndexAlertingCommentAction.kt | 78 ++++++++++++------- .../TransportSearchAlertingCommentAction.kt | 2 +- .../opensearch/alerting/util/CommentsUtils.kt | 20 +++-- 14 files changed, 112 insertions(+), 72 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index 16a2f1048..d01c60eb8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -685,7 +685,7 @@ class AlertService( val alertsIndex = dataSources.alertsIndex val alertsHistoryIndex = dataSources.alertsHistoryIndex - val commentsToDeleteIDs = mutableListOf() + val commentIDsToDelete = mutableListOf() var requestsToRetry = alerts.flatMap { alert -> // We don't want to set the version when saving alerts because the MonitorRunner has first priority when writing alerts. @@ -746,7 +746,7 @@ class AlertService( // Comments are stored in aliased history indices, not a concrete Comments // index like Alerts. A DeleteBy request will be used to delete Comments, instead // of a regular Delete request - commentsToDeleteIDs.addAll(CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id))) + commentIDsToDelete.addAll(CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id))) null } ) @@ -770,7 +770,7 @@ class AlertService( } // delete all the comments of any Alerts that were deleted - CommentsUtils.deleteComments(client, commentsToDeleteIDs) + CommentsUtils.deleteComments(client, commentIDsToDelete) } /** diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt index 92f7b5634..99bedd46f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/BucketLevelMonitorRunner.kt @@ -296,12 +296,17 @@ object BucketLevelMonitorRunner : MonitorRunner() { if (actionExecutionScope is PerAlertActionScope && !shouldDefaultToPerExecution) { for (alertCategory in actionExecutionScope.actionableAlerts) { val alertsToExecuteActionsFor = nextAlerts[trigger.id]?.get(alertCategory) ?: mutableListOf() + val alertsToExecuteActionsForIds = alertsToExecuteActionsFor.map { it.id } + val allAlertsComments = CommentsUtils.getCommentsForAlertNotification( + monitorCtx.client!!, + alertsToExecuteActionsForIds, + maxComments + ) for (alert in alertsToExecuteActionsFor) { - val alertComments = CommentsUtils.getCommentsForAlertNotification(monitorCtx.client!!, alert.id, maxComments) val alertContext = if (alertCategory != AlertCategory.NEW) { - AlertContext(alert = alert, comments = alertComments.ifEmpty { null }) + AlertContext(alert = alert, comments = allAlertsComments[alert.id]) } else { - getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, alertComments.ifEmpty { null }) + getAlertContext(alert = alert, alertSampleDocs = alertSampleDocs, allAlertsComments[alert.id]) } val actionCtx = getActionContextForAlertCategory( @@ -334,34 +339,27 @@ object BucketLevelMonitorRunner : MonitorRunner() { if (monitorOrTriggerError == null && dedupedAlerts.isEmpty() && newAlerts.isEmpty() && completedAlerts.isEmpty()) continue + val alertsToExecuteActionsForIds = dedupedAlerts.map { it.id } + .plus(newAlerts.map { it.id }) + .plus(completedAlerts.map { it.id }) + val allAlertsComments = CommentsUtils.getCommentsForAlertNotification( + monitorCtx.client!!, + alertsToExecuteActionsForIds, + maxComments + ) val actionCtx = triggerCtx.copy( dedupedAlerts = dedupedAlerts.map { - val dedupedAlertsComments = CommentsUtils.getCommentsForAlertNotification( - monitorCtx.client!!, - it.id, - maxComments - ) - AlertContext(alert = it, comments = dedupedAlertsComments.ifEmpty { null }) + AlertContext(alert = it, comments = allAlertsComments[it.id]) }, newAlerts = newAlerts.map { - val newAlertsComments = CommentsUtils.getCommentsForAlertNotification( - monitorCtx.client!!, - it.id, - maxComments - ) getAlertContext( alert = it, alertSampleDocs = alertSampleDocs, - alertComments = newAlertsComments.ifEmpty { null } + alertComments = allAlertsComments[it.id] ) }, completedAlerts = completedAlerts.map { - val completedAlertsComments = CommentsUtils.getCommentsForAlertNotification( - monitorCtx.client!!, - it.id, - maxComments - ) - AlertContext(alert = it, comments = completedAlertsComments.ifEmpty { null }) + AlertContext(alert = it, comments = allAlertsComments[it.id]) }, error = monitorResult.error ?: triggerResult.error ) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt index 40a1b1297..eb68d3607 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt @@ -117,6 +117,8 @@ abstract class MonitorRunner { ) } + // TODO: inspect actionResponseContent when message is super large see if an error is thrown somehow + // TODO: (spam {{ctx}} in notification content) var actionResponseContent = "" actionResponseContent = config.channel ?.sendNotification( diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt index f759021f4..fb5656ead 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/QueryLevelMonitorRunner.kt @@ -68,12 +68,18 @@ object QueryLevelMonitorRunner : MonitorRunner() { val updatedAlerts = mutableListOf() val triggerResults = mutableMapOf() + + val maxComments = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION) + val alertsToExecuteActionsForIds = currentAlerts.mapNotNull { it.value }.map { it.id } + val allAlertsComments = CommentsUtils.getCommentsForAlertNotification( + monitorCtx.client!!, + alertsToExecuteActionsForIds, + maxComments + ) for (trigger in monitor.triggers) { val currentAlert = currentAlerts[trigger] val currentAlertContext = currentAlert?.let { - val maxComments = monitorCtx.clusterService!!.clusterSettings.get(AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION) - val currentAlertComments = CommentsUtils.getCommentsForAlertNotification(monitorCtx.client!!, currentAlert.id, maxComments) - AlertContext(alert = currentAlert, comments = currentAlertComments.ifEmpty { null }) + AlertContext(alert = currentAlert, comments = allAlertsComments[currentAlert.id]) } val triggerCtx = QueryLevelTriggerExecutionContext(monitor, trigger as QueryLevelTrigger, monitorResult, currentAlertContext) val triggerResult = when (Monitor.MonitorType.valueOf(monitor.monitorType.uppercase(Locale.ROOT))) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt index b858d1a51..64c62f926 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/alerts/AlertIndices.kt @@ -610,8 +610,8 @@ class AlertIndices( private suspend fun deleteAlertComments(alertHistoryIndicesToDelete: List) { alertHistoryIndicesToDelete.forEach { alertHistoryIndex -> val alertIDs = getAlertIDsFromAlertHistoryIndex(alertHistoryIndex) - val commentsToDeleteIDs = CommentsUtils.getCommentIDsByAlertIDs(client, alertIDs) - CommentsUtils.deleteComments(client, commentsToDeleteIDs) + val commentIDsToDelete = CommentsUtils.getCommentIDsByAlertIDs(client, alertIDs) + CommentsUtils.deleteComments(client, commentIDsToDelete) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt index 38050e62a..c473586ed 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt @@ -110,7 +110,7 @@ class CommentsIndices( * @param actionListener A callback listener for the index creation call. Generally in the form of onSuccess, onFailure */ - fun onMaster() { + fun onManager() { try { // try to rollover immediately as we might be restarting the cluster rolloverCommentsHistoryIndex() @@ -126,7 +126,7 @@ class CommentsIndices( } } - fun offMaster() { + fun offManager() { scheduledCommentsRollover?.cancel() } @@ -141,9 +141,9 @@ class CommentsIndices( if (this.isClusterManager != event.localNodeClusterManager()) { this.isClusterManager = event.localNodeClusterManager() if (this.isClusterManager) { - onMaster() + onManager() } else { - offMaster() + offManager() } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt index c73393819..024b79970 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestDeleteAlertingCommentAction.kt @@ -39,7 +39,7 @@ class RestDeleteAlertingCommentAction : BaseRestHandler() { @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/{id}") + log.info("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/{id}") val commentId = request.param("id") val deleteMonitorRequest = DeleteCommentRequest(commentId) return RestChannelConsumer { channel -> diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index 2a13ed800..c65fe99b2 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -54,7 +54,7 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}") + log.info("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}") val id = request.param( "id", @@ -69,7 +69,7 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { val alertId = if (request.method() == RestRequest.Method.POST) id else Alert.NO_ID val commentId = if (request.method() == RestRequest.Method.PUT) id else Comment.NO_ID - val content = request.contentParser().map()["content"] as String? + val content = request.contentParser().map()[Comment.COMMENT_CONTENT_FIELD] as String? if (content.isNullOrEmpty()) { throw AlertingException.wrap(IllegalArgumentException("Missing comment content")) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt index 24ba6e9af..1fda3683f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt @@ -57,7 +57,7 @@ class RestSearchAlertingCommentAction() : BaseRestHandler() { @Throws(IOException::class) override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - log.debug("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/_search") + log.info("${request.method()} ${AlertingPlugin.COMMENTS_BASE_URI}/_search") val searchSourceBuilder = SearchSourceBuilder() searchSourceBuilder.parseXContent(request.contentOrSourceParamParser()) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt index 5cc9cbd34..57f7d8b72 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt @@ -126,7 +126,7 @@ class RestSearchMonitorAction( } } } catch (e: Exception) { - log.info("The monitor parsing failed. Will return response as is.") + log.debug("The monitor parsing failed. Will return response as is. Parsing error: $e") } return BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), EMPTY_PARAMS)) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index 2b9a5913a..7496603ed 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -159,7 +159,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( if (searchResponse.hits.totalHits.value == 0L) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Comment with ID $commentId is not found", RestStatus.NOT_FOUND) + OpenSearchStatusException("Comment not found", RestStatus.NOT_FOUND) ) ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index 2a75c739e..d5c48f2bc 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -11,8 +11,6 @@ import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionRequest -import org.opensearch.action.get.GetRequest -import org.opensearch.action.get.GetResponse import org.opensearch.action.index.IndexRequest import org.opensearch.action.index.IndexResponse import org.opensearch.action.search.SearchRequest @@ -157,7 +155,7 @@ constructor( if (alert == null) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Alert with ID ${request.entityId} is not found", RestStatus.NOT_FOUND), + OpenSearchStatusException("Alert not found", RestStatus.NOT_FOUND), ) ) return @@ -176,7 +174,7 @@ constructor( return } - log.info("checking user permissions in index comment") + log.debug("checking user permissions in index comment") checkUserPermissionsWithResource(user, alert.monitorUser, actionListener, "monitor", alert.monitorId) val comment = Comment(entityId = request.entityId, content = request.content, createdTime = Instant.now(), user = user) @@ -190,7 +188,7 @@ constructor( .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) // .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) - log.info("Creating new comment: ${comment.toXContentWithUser(XContentFactory.jsonBuilder())}") + log.debug("Creating new comment: ${comment.toXContentWithUser(XContentFactory.jsonBuilder())}") try { val indexResponse: IndexResponse = client.suspendUntil { client.index(indexRequest, it) } @@ -215,7 +213,7 @@ constructor( if (currentComment == null) { actionListener.onFailure( AlertingException.wrap( - OpenSearchStatusException("Comment with ID ${request.commentId} is not found", RestStatus.NOT_FOUND), + OpenSearchStatusException("Comment not found", RestStatus.NOT_FOUND), ), ) return @@ -228,8 +226,7 @@ constructor( actionListener.onFailure( AlertingException.wrap( OpenSearchStatusException( - "Comment ${request.commentId} created by ${currentComment.user} " + - "can only be edited by Admin or ${currentComment.user} ", + "Comment can only be edited by Admin or author of comment", RestStatus.FORBIDDEN, ), ), @@ -250,7 +247,7 @@ constructor( .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) // .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) - log.info( + log.debug( "Updating comment, ${currentComment.id}, from: " + "${currentComment.content} to: " + requestComment.content, @@ -312,30 +309,57 @@ constructor( } if (alerts.isEmpty()) return null + if (alerts.size > 1) { + actionListener.onFailure( + AlertingException.wrap(IllegalStateException("Multiple alerts were found with the same ID")), + ) + return null + } - return alerts[0] // there should only be 1 Alert that matched the request alert ID + return alerts[0] } private suspend fun getComment(): Comment? { - val getRequest = GetRequest(COMMENTS_HISTORY_WRITE_INDEX, request.commentId) - try { - val getResponse: GetResponse = client.suspendUntil { client.get(getRequest, it) } - if (!getResponse.isExists) return null - val xcp = - XContentHelper.createParser( - xContentRegistry, - LoggingDeprecationHandler.INSTANCE, - getResponse.sourceAsBytesRef, - XContentType.JSON, - ) - xcp.nextToken() - val comment = Comment.parse(xcp, getResponse.id) - log.info("comment to be updated: $comment") - return comment - } catch (t: Exception) { - actionListener.onFailure(AlertingException.wrap(t)) + // need to validate the existence of the Alert that user is trying to add Comment to. + // Also need to check if user has permissions to add a Comment to the passed in Alert. To do this, + // we retrieve the Alert to get its associated monitor user, and use that to + // check if they have permissions to the Monitor that generated the Alert + val queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("_id", listOf(request.commentId))) + val searchSourceBuilder = + SearchSourceBuilder() + .version(true) + .seqNoAndPrimaryTerm(true) + .query(queryBuilder) + + // search all alerts, since user might want to create a comment + // on a completed alert + val searchRequest = + SearchRequest() + .indices(CommentsIndices.ALL_COMMENTS_INDEX_PATTERN) + .source(searchSourceBuilder) + + val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } + val comments = searchResponse.hits.map { hit -> + val xcp = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + hit.sourceRef, + XContentType.JSON + ) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val comment = Comment.parse(xcp, hit.id) + comment + } + + if (comments.isEmpty()) return null + if (comments.size > 1) { + actionListener.onFailure( + AlertingException.wrap(IllegalStateException("Multiple comments were found with the same ID")), + ) return null } + + return comments[0] } private fun checkShardsFailure(response: IndexResponse): String? { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt index 525aa25a7..e15a5759f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt @@ -114,7 +114,7 @@ class TransportSearchAlertingCommentAction @Inject constructor( } else { // security is enabled and filterby is enabled. try { - log.info("Filtering result by: ${user.backendRoles}") + log.debug("Filtering result by: {}", user.backendRoles) // first retrieve all Alert IDs current User can see after filtering by backend roles val alertIDs = getFilteredAlertIDs(user) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt index 1a2b26103..15cc50c27 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/CommentsUtils.kt @@ -73,13 +73,23 @@ class CommentsUtils { * Performs a Search request to retrieve the top maxComments most recent comments associated with the * given Alert, where maxComments is a cluster setting. */ - suspend fun getCommentsForAlertNotification(client: Client, alertId: String, maxComments: Int): List { - val allComments = getCommentsByAlertIDs(client, listOf(alertId)) + suspend fun getCommentsForAlertNotification( + client: Client, + alertIds: List, + maxComments: Int + ): Map> { + val allComments = getCommentsByAlertIDs(client, alertIds) val sortedComments = allComments.sortedByDescending { it.createdTime } - if (sortedComments.size <= maxComments) { - return sortedComments + val alertIdToComments = mutableMapOf>() + for (comment in sortedComments) { + if (!alertIdToComments.containsKey(comment.entityId)) { + alertIdToComments[comment.entityId] = mutableListOf() + } else if (alertIdToComments[comment.entityId]!!.size >= maxComments) { + continue + } + alertIdToComments[comment.entityId]!!.add(comment) } - return sortedComments.slice(0 until maxComments) + return alertIdToComments.mapValues { it.value.toList() } } // Deletes all Comments given by the list of Comments IDs From 71dce54d147218b0c2f5b2105bba5fe9de34a8f7 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 10 Jun 2024 11:26:39 -0700 Subject: [PATCH 16/22] update after adding entityType to Comment model Signed-off-by: Dennis Toepker --- .../resthandler/RestIndexAlertingCommentAction.kt | 10 +++++++++- .../transport/TransportIndexAlertingCommentAction.kt | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index c65fe99b2..c5529d712 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -76,7 +76,15 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { val seqNo = request.paramAsLong(IF_SEQ_NO, SequenceNumbers.UNASSIGNED_SEQ_NO) val primaryTerm = request.paramAsLong(IF_PRIMARY_TERM, SequenceNumbers.UNASSIGNED_PRIMARY_TERM) - val indexCommentRequest = IndexCommentRequest(alertId, commentId, seqNo, primaryTerm, request.method(), content) + val indexCommentRequest = IndexCommentRequest( + alertId, + Comment.EntityType.ALERT.value, + commentId, + seqNo, + primaryTerm, + request.method(), + content + ) return RestChannelConsumer { channel -> client.execute(AlertingActions.INDEX_COMMENT_ACTION_TYPE, indexCommentRequest, indexCommentResponse(channel, request.method())) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index d5c48f2bc..2add00f32 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -177,7 +177,13 @@ constructor( log.debug("checking user permissions in index comment") checkUserPermissionsWithResource(user, alert.monitorUser, actionListener, "monitor", alert.monitorId) - val comment = Comment(entityId = request.entityId, content = request.content, createdTime = Instant.now(), user = user) + val comment = Comment( + entityId = request.entityId, + entityType = request.entityType, + content = request.content, + createdTime = Instant.now(), + user = user + ) val indexRequest = IndexRequest(COMMENTS_HISTORY_WRITE_INDEX) From ef31e28a634a8714f5a3be96f35d9d214b49cabf Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Mon, 10 Jun 2024 20:03:31 -0700 Subject: [PATCH 17/22] misc fixes Signed-off-by: Dennis Toepker --- .../RestIndexAlertingCommentAction.kt | 2 +- .../alerting/settings/AlertingSettings.kt | 6 +-- .../TransportDeleteAlertingCommentAction.kt | 27 ++++++---- .../TransportIndexAlertingCommentAction.kt | 54 ++++++++++--------- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt index c5529d712..9dc2cb756 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexAlertingCommentAction.kt @@ -78,7 +78,7 @@ class RestIndexAlertingCommentAction : BaseRestHandler() { val indexCommentRequest = IndexCommentRequest( alertId, - Comment.EntityType.ALERT.value, + "alert", commentId, seqNo, primaryTerm, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index 9f4212370..3216dd781 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -262,21 +262,21 @@ class AlertingSettings { ) val COMMENTS_MAX_CONTENT_SIZE = Setting.longSetting( - "plugins.alerting.comments.max_content_size", + "plugins.alerting.max_comment_character_length", 2000L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic ) val MAX_COMMENTS_PER_ALERT = Setting.longSetting( - "plugins.alerting.comments.max_comments_per_alert", + "plugins.alerting.max_comments_per_alert", 500L, 0L, Setting.Property.NodeScope, Setting.Property.Dynamic ) val MAX_COMMENTS_PER_NOTIFICATION = Setting.intSetting( - "plugins.alerting.comments.max_comments_per_notification", + "plugins.alerting.max_comments_per_notification", 3, 0, Setting.Property.NodeScope, Setting.Property.Dynamic diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index 7496603ed..f31510d52 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -108,7 +108,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( suspend fun resolveUserAndStart() { try { - val comment = getComment() + val comment = getComment() ?: return if (sourceIndex == null) { actionListener.onFailure( @@ -142,7 +142,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( } } - private suspend fun getComment(): Comment { + private suspend fun getComment(): Comment? { val queryBuilder = QueryBuilders .boolQuery() .must(QueryBuilders.termsQuery("_id", commentId)) @@ -156,13 +156,6 @@ class TransportDeleteAlertingCommentAction @Inject constructor( .indices(ALL_COMMENTS_INDEX_PATTERN) val searchResponse: SearchResponse = client.suspendUntil { search(searchRequest, it) } - if (searchResponse.hits.totalHits.value == 0L) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException("Comment not found", RestStatus.NOT_FOUND) - ) - ) - } val comments = searchResponse.hits.map { hit -> val xcp = XContentHelper.createParser( NamedXContentRegistry.EMPTY, @@ -176,7 +169,21 @@ class TransportDeleteAlertingCommentAction @Inject constructor( comment } - return comments[0] // we searched on Comment ID, there should only be one Comment in the List + if (comments.isEmpty()) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Comment not found", RestStatus.NOT_FOUND), + ), + ) + return null + } else if (comments.size > 1) { + actionListener.onFailure( + AlertingException.wrap(IllegalStateException("Multiple comments were found with the same ID")), + ) + return null + } + + return comments[0] } } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index 2add00f32..43a3ed147 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -126,6 +126,18 @@ constructor( return } + // validate the request is for the correct entity type + if (transformedRequest.entityType != "alert") { + actionListener.onFailure( + AlertingException.wrap( + IllegalArgumentException( + "Index comment request is for wrong entity type, expected alert, got ${transformedRequest.entityType}" + ) + ) + ) + return + } + val user = readUserFromThreadContext(client) client.threadPool().threadContext.stashContext().use { @@ -151,15 +163,7 @@ constructor( } private suspend fun indexComment() { - val alert = getAlert() - if (alert == null) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException("Alert not found", RestStatus.NOT_FOUND), - ) - ) - return - } + val alert = getAlert() ?: return val numCommentsOnThisAlert = CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id)).size if (numCommentsOnThisAlert >= maxCommentsPerAlert) { @@ -192,7 +196,6 @@ constructor( .setIfPrimaryTerm(request.primaryTerm) .timeout(indexTimeout) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) -// .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) log.debug("Creating new comment: ${comment.toXContentWithUser(XContentFactory.jsonBuilder())}") @@ -215,15 +218,7 @@ constructor( } private suspend fun updateComment() { - val currentComment = getComment() - if (currentComment == null) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException("Comment not found", RestStatus.NOT_FOUND), - ), - ) - return - } + val currentComment = getComment() ?: return // check that the user has permissions to edit the comment. user can edit comment if // - user is Admin @@ -251,7 +246,6 @@ constructor( .setIfPrimaryTerm(request.primaryTerm) .timeout(indexTimeout) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) -// .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) log.debug( "Updating comment, ${currentComment.id}, from: " + @@ -314,8 +308,14 @@ constructor( alert } - if (alerts.isEmpty()) return null - if (alerts.size > 1) { + if (alerts.isEmpty()) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Alert not found", RestStatus.NOT_FOUND), + ) + ) + return null + } else if (alerts.size > 1) { actionListener.onFailure( AlertingException.wrap(IllegalStateException("Multiple alerts were found with the same ID")), ) @@ -357,8 +357,14 @@ constructor( comment } - if (comments.isEmpty()) return null - if (comments.size > 1) { + if (comments.isEmpty()) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("Comment not found", RestStatus.NOT_FOUND), + ), + ) + return null + } else if (comments.size > 1) { actionListener.onFailure( AlertingException.wrap(IllegalStateException("Multiple comments were found with the same ID")), ) From acc876d235fa1390e7b4559709cb63512c5c9d80 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 11 Jun 2024 12:25:21 -0700 Subject: [PATCH 18/22] removing comments history enabled setting Signed-off-by: Dennis Toepker --- .../main/kotlin/org/opensearch/alerting/AlertingPlugin.kt | 1 - .../org/opensearch/alerting/comments/CommentsIndices.kt | 7 ------- .../org/opensearch/alerting/settings/AlertingSettings.kt | 6 ------ 3 files changed, 14 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index d2dc22317..4ca201f60 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -406,7 +406,6 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.FINDINGS_INDEXING_BATCH_SIZE, AlertingSettings.CROSS_CLUSTER_MONITORING_ENABLED, AlertingSettings.ALERTING_COMMENTS_ENABLED, - AlertingSettings.COMMENTS_HISTORY_ENABLED, AlertingSettings.COMMENTS_HISTORY_MAX_DOCS, AlertingSettings.COMMENTS_HISTORY_INDEX_MAX_AGE, AlertingSettings.COMMENTS_HISTORY_ROLLOVER_PERIOD, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt index c473586ed..6bbeee933 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/comments/CommentsIndices.kt @@ -50,7 +50,6 @@ class CommentsIndices( ) : ClusterStateListener { init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_ENABLED) { commentsHistoryEnabled = it } clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_MAX_DOCS) { commentsHistoryMaxDocs = it } clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.COMMENTS_HISTORY_INDEX_MAX_AGE) { commentsHistoryMaxAge = it @@ -84,8 +83,6 @@ class CommentsIndices( private val logger = LogManager.getLogger(AlertIndices::class.java) } - @Volatile private var commentsHistoryEnabled = AlertingSettings.COMMENTS_HISTORY_ENABLED.get(settings) - @Volatile private var commentsHistoryMaxDocs = AlertingSettings.COMMENTS_HISTORY_MAX_DOCS.get(settings) @Volatile private var commentsHistoryMaxAge = AlertingSettings.COMMENTS_HISTORY_INDEX_MAX_AGE.get(settings) @@ -163,10 +160,6 @@ class CommentsIndices( return clusterService.state().metadata.hasAlias(COMMENTS_HISTORY_WRITE_INDEX) } - fun isCommentsHistoryEnabled(): Boolean { - return commentsHistoryEnabled - } - suspend fun createOrUpdateInitialCommentsHistoryIndex() { if (!isCommentsHistoryInitialized()) { commentsHistoryIndexInitialized = createIndex(COMMENTS_HISTORY_INDEX_PATTERN, commentsMapping(), COMMENTS_HISTORY_WRITE_INDEX) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index 3216dd781..8ab7eee24 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -230,12 +230,6 @@ class AlertingSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ) - val COMMENTS_HISTORY_ENABLED = Setting.boolSetting( - "plugins.alerting.comments_history_enabled", - true, - Setting.Property.NodeScope, Setting.Property.Dynamic - ) - val COMMENTS_HISTORY_MAX_DOCS = Setting.longSetting( "plugins.alerting.comments_history_max_docs", 1000L, From 612d0fc235f10ca93db25d9c52c094773c6d1e14 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 11 Jun 2024 14:38:49 -0700 Subject: [PATCH 19/22] adding release notes Signed-off-by: Dennis Toepker --- release-notes/opensearch-alerting.release-notes-2.15.0.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes/opensearch-alerting.release-notes-2.15.0.0.md b/release-notes/opensearch-alerting.release-notes-2.15.0.0.md index 3f2fed10c..d933c3413 100644 --- a/release-notes/opensearch-alerting.release-notes-2.15.0.0.md +++ b/release-notes/opensearch-alerting.release-notes-2.15.0.0.md @@ -10,6 +10,7 @@ Compatible with OpenSearch 2.15.0 ### Enhancements * Add start_time and end_time to GetAlertsRequest. ([#1551](https://github.com/opensearch-project/alerting/pull/1551)) +* Add Alerting Comments experimental feature ([#1561](https://github.com/opensearch-project/alerting/pull/1561)) ### Documentation * Added 2.15 release notes. ([#1569](https://github.com/opensearch-project/alerting/pull/1569)) \ No newline at end of file From 933474ddf2938c90420b7b00a7faab9196b199b7 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 11 Jun 2024 15:17:58 -0700 Subject: [PATCH 20/22] IT fixes Signed-off-by: Dennis Toepker --- .../src/main/kotlin/org/opensearch/alerting/AlertService.kt | 6 +++--- .../kotlin/org/opensearch/alerting/AlertingRestTestCase.kt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt index d01c60eb8..fc54a0375 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertService.kt @@ -685,7 +685,7 @@ class AlertService( val alertsIndex = dataSources.alertsIndex val alertsHistoryIndex = dataSources.alertsHistoryIndex - val commentIDsToDelete = mutableListOf() + val commentIdsToDelete = mutableListOf() var requestsToRetry = alerts.flatMap { alert -> // We don't want to set the version when saving alerts because the MonitorRunner has first priority when writing alerts. @@ -746,7 +746,7 @@ class AlertService( // Comments are stored in aliased history indices, not a concrete Comments // index like Alerts. A DeleteBy request will be used to delete Comments, instead // of a regular Delete request - commentIDsToDelete.addAll(CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id))) + commentIdsToDelete.addAll(CommentsUtils.getCommentIDsByAlertIDs(client, listOf(alert.id))) null } ) @@ -770,7 +770,7 @@ class AlertService( } // delete all the comments of any Alerts that were deleted - CommentsUtils.deleteComments(client, commentIDsToDelete) + CommentsUtils.deleteComments(client, commentIdsToDelete) } /** diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index 45cc34474..b5fd994df 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -548,6 +548,7 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { return Comment( id = commentId, entityId = comment["entity_id"] as String, + entityType = comment["entity_type"] as String, content = comment["content"] as String, createdTime = Instant.ofEpochMilli(comment["created_time"] as Long), lastUpdatedTime = if (comment["last_updated_time"] != null) { From 5a9e9ce503cbb3bcabdba83796e8344874c9df2b Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 11 Jun 2024 15:40:23 -0700 Subject: [PATCH 21/22] removing dev code vestiges Signed-off-by: Dennis Toepker --- .../src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt | 2 -- .../opensearch/alerting/resthandler/RestSearchMonitorAction.kt | 2 +- .../src/test/kotlin/org/opensearch/alerting/TestHelpers.kt | 1 - .../org/opensearch/alerting/resthandler/MonitorRestApiIT.kt | 3 --- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt index eb68d3607..40a1b1297 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/MonitorRunner.kt @@ -117,8 +117,6 @@ abstract class MonitorRunner { ) } - // TODO: inspect actionResponseContent when message is super large see if an error is thrown somehow - // TODO: (spam {{ctx}} in notification content) var actionResponseContent = "" actionResponseContent = config.channel ?.sendNotification( diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt index 57f7d8b72..c22f5aa68 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt @@ -126,7 +126,7 @@ class RestSearchMonitorAction( } } } catch (e: Exception) { - log.debug("The monitor parsing failed. Will return response as is. Parsing error: $e") + log.debug("The monitor parsing failed. Will return response as is.") } return BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), EMPTY_PARAMS)) } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index bf1d903dd..ee4a5ece3 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -402,7 +402,6 @@ val WORKFLOW_ALERTING_BASE_URI = "/_plugins/_alerting/workflows" val DESTINATION_BASE_URI = "/_plugins/_alerting/destinations" val LEGACY_OPENDISTRO_ALERTING_BASE_URI = "/_opendistro/_alerting/monitors" val LEGACY_OPENDISTRO_DESTINATION_BASE_URI = "/_opendistro/_alerting/destinations" -val ALERTING_NOTES_BASE_URI = "" val ALWAYS_RUN = Script("return true") val NEVER_RUN = Script("return false") val DRYRUN_MONITOR = mapOf("dryrun" to "true") diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt index 2510ad8b5..79c871a97 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/MonitorRestApiIT.kt @@ -1147,12 +1147,9 @@ class MonitorRestApiIT : AlertingRestTestCase() { assertEquals("Search monitor failed", RestStatus.OK, searchResponse.restStatus()) val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content) val hits = xcp.map()["hits"]!! as Map> - logger.info("hits: $hits") val numberDocsFound = hits["total"]?.get("value") assertEquals("Destination objects are also returned by /_search.", 1, numberDocsFound) - assertEquals("fdsafds", false, true) - val searchHits = hits["hits"] as List val hit = searchHits[0] as Map val monitorHit = hit["_source"] as Map From 25560580410e683e84abd8971876cfa80cfb1416 Mon Sep 17 00:00:00 2001 From: Dennis Toepker Date: Tue, 11 Jun 2024 17:02:03 -0700 Subject: [PATCH 22/22] changing logger calls Signed-off-by: Dennis Toepker --- .../alerting/resthandler/RestSearchAlertingCommentAction.kt | 2 +- .../opensearch/alerting/resthandler/RestSearchMonitorAction.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt index 1fda3683f..821d6639e 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchAlertingCommentAction.kt @@ -96,7 +96,7 @@ class RestSearchAlertingCommentAction() : BaseRestHandler() { } } } catch (e: Exception) { - log.info("The comment parsing failed. Will return response as is.") + log.error("The comment parsing failed. Will return response as is.") } return BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), EMPTY_PARAMS)) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt index c22f5aa68..6df5641e9 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestSearchMonitorAction.kt @@ -126,7 +126,7 @@ class RestSearchMonitorAction( } } } catch (e: Exception) { - log.debug("The monitor parsing failed. Will return response as is.") + log.error("The monitor parsing failed. Will return response as is.") } return BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), EMPTY_PARAMS)) }