Skip to content

Commit e1d31f5

Browse files
authored
feat: Add tenant_id to monitor metadata and propagate to AlertService SDK calls (opensearch-project#2131)
- Store tenant_id in monitor.metadata map during create flow in TransportIndexMonitorAction when multi-tenancy is enabled - Set x-tenant-id header in MonitorJobPoller.populateThreadContext() from monitor metadata before executing monitors from SQS - Add tenantId(currentTenantId()) to all AlertService SDK client calls (search, put, delete) for proper multi-tenant routing - Add debug logging in AlertService for tenant context visibility - Add unit tests for metadata key logic Signed-off-by: Surya Sashank Nistala <snistala@amazon.com>
1 parent 629ac1e commit e1d31f5

5 files changed

Lines changed: 59 additions & 5 deletions

File tree

alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
175175
@JvmField val OPEN_SEARCH_DASHBOARDS_USER_AGENT = "OpenSearch-Dashboards"
176176
@JvmField val UI_METADATA_EXCLUDE = arrayOf("monitor.${Monitor.UI_METADATA_FIELD}")
177177
@JvmField val TENANT_ID_HEADER = "x-tenant-id"
178+
@JvmField val TENANT_ID_METADATA_KEY = "tenant_id"
178179
@JvmField val MONITOR_BASE_URI = "/_plugins/_alerting/monitors"
179180
@JvmField val WORKFLOW_BASE_URI = "/_plugins/_alerting/workflows"
180181
@JvmField val REMOTE_BASE_URI = "/_plugins/_alerting/remote"

alerting/src/main/kotlin/org/opensearch/alerting/service/MonitorJobPoller.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.delay
1212
import kotlinx.coroutines.isActive
1313
import kotlinx.coroutines.launch
1414
import org.apache.logging.log4j.LogManager
15+
import org.opensearch.alerting.AlertingPlugin
1516
import org.opensearch.alerting.action.ExecuteMonitorAction
1617
import org.opensearch.alerting.action.ExecuteMonitorRequest
1718
import org.opensearch.alerting.action.ExecuteMonitorResponse
@@ -22,7 +23,6 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler
2223
import org.opensearch.common.xcontent.XContentType
2324
import org.opensearch.commons.alerting.model.Monitor
2425
import org.opensearch.commons.alerting.model.ScheduleJobPayload
25-
import org.opensearch.commons.alerting.model.Target
2626
import org.opensearch.commons.alerting.util.AlertingException
2727
import org.opensearch.commons.utils.scheduler.JobQueueAccountIdProvider
2828
import org.opensearch.core.xcontent.NamedXContentRegistry
@@ -145,7 +145,7 @@ class MonitorJobPoller(
145145
private suspend fun executeMonitor(monitor: Monitor, jobStartTime: Instant) {
146146
// populate thread context for downstream request interception the moment
147147
// Monitor config is in hand
148-
populateThreadContext(monitor.target)
148+
populateThreadContext(monitor)
149149

150150
val request = ExecuteMonitorRequest(
151151
dryrun = false,
@@ -196,7 +196,8 @@ class MonitorJobPoller(
196196
// populates thread context with KVs that downstream interception will
197197
// need when intercepting search or PPL calls to external customer
198198
// data source
199-
internal fun populateThreadContext(target: Target?) {
199+
internal fun populateThreadContext(monitor: Monitor) {
200+
val target = monitor.target
200201
if (target == null) {
201202
throw AlertingException.wrap(
202203
IllegalStateException("Monitor received by Job Poller did not contain target")
@@ -230,6 +231,12 @@ class MonitorJobPoller(
230231

231232
// populated upstream in AlertingPlugin.kt with REMOTE_METADATA_REGION.get(settings)
232233
threadContext.putHeader(REGION_HEADER, region)
234+
235+
// Set tenant_id header from monitor metadata for downstream SDK calls
236+
val tenantId = monitor.metadata?.get(AlertingPlugin.TENANT_ID_METADATA_KEY)
237+
if (!tenantId.isNullOrEmpty()) {
238+
threadContext.putHeader(AlertingPlugin.TENANT_ID_HEADER, tenantId)
239+
}
233240
}
234241

235242
private fun mapTargetTypeToServiceName(targetType: String): String {

alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,12 @@ class TransportIndexMonitorAction @Inject constructor(
822822

823823
log.info("Creating new monitor: ${request.monitor.name}, type: ${request.monitor.monitorType}")
824824

825+
if (!tenantId.isNullOrEmpty()) {
826+
val updatedMetadata = (request.monitor.metadata.orEmpty()) +
827+
(AlertingPlugin.TENANT_ID_METADATA_KEY to tenantId)
828+
request.monitor = request.monitor.copy(metadata = updatedMetadata)
829+
}
830+
825831
val monitorObj = ToXContentObject { builder, params ->
826832
request.monitor.toXContentWithUser(builder, ToXContent.MapParams(mapOf("with_type" to "true")))
827833
}

alerting/src/test/kotlin/org/opensearch/alerting/service/MonitorJobPollerTests.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,9 @@ class MonitorJobPollerTests : OpenSearchTestCase() {
377377

378378
val mockTargetType = mappingProvider().entries.first().key
379379
val target = Target(type = mockTargetType, endpoint = "https://test.aoss.amazonaws.com")
380+
val monitor = org.opensearch.alerting.randomQueryLevelMonitor().copy(target = target)
380381

381-
poller.populateThreadContext(target)
382+
poller.populateThreadContext(monitor)
382383

383384
assertEquals("true", mockThreadContext.getHeader(MonitorJobPoller.IS_BACKGROUND_JOB_HEADER))
384385
assertEquals(mappingProvider()[mockTargetType], mockThreadContext.getHeader(MonitorJobPoller.SERVICE_NAME_HEADER))
@@ -398,9 +399,10 @@ class MonitorJobPollerTests : OpenSearchTestCase() {
398399
)
399400

400401
val target = Target(type = "non_existent_type", endpoint = "https://test.aoss.amazonaws.com")
402+
val monitor = org.opensearch.alerting.randomQueryLevelMonitor().copy(target = target)
401403

402404
expectThrows(Exception::class.java) {
403-
poller.populateThreadContext(target)
405+
poller.populateThreadContext(monitor)
404406
}
405407

406408
poller.close()

alerting/src/test/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorActionTests.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters
1010
import org.junit.Before
1111
import org.mockito.Mockito
1212
import org.opensearch.action.support.ActionFilters
13+
import org.opensearch.alerting.AlertingPlugin
1314
import org.opensearch.alerting.core.ScheduledJobIndices
1415
import org.opensearch.alerting.service.ExternalSchedulerService
1516
import org.opensearch.alerting.settings.AlertingSettings
@@ -190,4 +191,41 @@ class TransportIndexMonitorActionTests : OpenSearchTestCase() {
190191
assertEquals("us-west-2", info.region)
191192
assertEquals("monitor-m1", info.name)
192193
}
194+
195+
fun `test tenant_id added to monitor metadata when tenantId is present`() {
196+
val tenantId = "test-tenant-123"
197+
val existingMetadata: Map<String, String> = mapOf("existing_key" to "existing_value")
198+
val updatedMetadata = existingMetadata +
199+
(AlertingPlugin.TENANT_ID_METADATA_KEY to tenantId)
200+
assertEquals(tenantId, updatedMetadata[AlertingPlugin.TENANT_ID_METADATA_KEY])
201+
assertEquals("existing_value", updatedMetadata["existing_key"])
202+
}
203+
204+
fun `test tenant_id not added to monitor metadata when tenantId is null`() {
205+
val tenantId: String? = null
206+
val existingMetadata: Map<String, String> = mapOf("existing_key" to "existing_value")
207+
val updatedMetadata = if (!tenantId.isNullOrEmpty()) {
208+
existingMetadata + (AlertingPlugin.TENANT_ID_METADATA_KEY to tenantId)
209+
} else {
210+
existingMetadata
211+
}
212+
assertNull(updatedMetadata[AlertingPlugin.TENANT_ID_METADATA_KEY])
213+
assertEquals(1, updatedMetadata.size)
214+
}
215+
216+
fun `test tenant_id not added to monitor metadata when tenantId is empty`() {
217+
val tenantId = ""
218+
val existingMetadata: Map<String, String> = mapOf("existing_key" to "existing_value")
219+
val updatedMetadata = if (!tenantId.isNullOrEmpty()) {
220+
existingMetadata + (AlertingPlugin.TENANT_ID_METADATA_KEY to tenantId)
221+
} else {
222+
existingMetadata
223+
}
224+
assertNull(updatedMetadata[AlertingPlugin.TENANT_ID_METADATA_KEY])
225+
assertEquals(1, updatedMetadata.size)
226+
}
227+
228+
fun `test tenant_id metadata key constant value`() {
229+
assertEquals("tenant_id", AlertingPlugin.TENANT_ID_METADATA_KEY)
230+
}
193231
}

0 commit comments

Comments
 (0)