-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[Alerting] Allow tags to be added to alerts in bulk #241051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Alerting] Allow tags to be added to alerts in bulk #241051
Conversation
|
/ci |
4198cb9 to
bc8fbcf
Compare
|
Pinging @elastic/obs-ux-management-team (Team:obs-ux-management) |
|
Pinging @elastic/response-ops (Team:ResponseOps) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the bulk update alerts functionality by adding support for adding and removing tags, making the status parameter optional, and refactoring the implementation to use Painless scripts with parameters. The changes enable more flexible alert management operations.
Key Changes
- Added support for
addTagsandremoveTagsoperations in bulk alert updates - Made the
statusparameter optional in bulk update operations - Refactored update logic to use parameterized Painless scripts for better security
- Extracted Painless update scripts into a separate utility file for reusability
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
alert_client_bulk_update_scripts.ts |
New utility file containing Painless scripts for status and tag updates |
bulk_update_alerts.ts |
Updated route validation to support optional status and new tag operations using t.intersection and t.partial |
alerts_client.ts |
Refactored bulkUpdate method to build dynamic scripts, replaced mgetAlertsAuditOperateStatus with inline implementation using scripts |
alerts_client.test.ts |
Added comprehensive test coverage for tag operations and edge cases |
x-pack/platform/plugins/shared/rule_registry/server/utils/alert_client_bulk_update_scripts.ts
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| if (bulkUpdateRequest.length === 0) { | ||
| return; |
Copilot
AI
Oct 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When all alerts are not found (bulkUpdateRequest.length === 0), the method returns undefined without indicating whether this is an error condition. Consider returning a more informative response object (e.g., { updated: 0, errors: [] }) or logging a warning to help with debugging and monitoring.
| return; | |
| this.logger.warn( | |
| `bulkUpdate: No alerts found to update for ids=[${ids.join(', ')}] in index=${index}` | |
| ); | |
| return { updated: 0, errors: [], message: 'No alerts found to update.' }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably should be a debug log rather than a warn for a 404
| // @ts-expect-error doesn't handle error branch in MGetResponse | ||
| if (item.found) { |
Copilot
AI
Oct 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @ts-expect-error comment indicates a typing issue with MGetResponse. Consider using a type guard or proper type assertion instead of suppressing the error. For example: if ('found' in item && item.found) or use a proper type predicate to handle the union type correctly.
| // @ts-expect-error doesn't handle error branch in MGetResponse | |
| if (item.found) { | |
| // Only process items where the document was found | |
| if ('found' in item && item.found) { |
| t.union([ | ||
| t.strict({ | ||
| status: t.union([ | ||
| t.literal('open'), | ||
| t.literal('closed'), | ||
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | ||
| t.literal('acknowledged'), | ||
| ]), | ||
| index: t.string, | ||
| ids: t.array(t.string), | ||
| query: t.undefined, | ||
| }), | ||
| t.strict({ | ||
| status: t.union([ | ||
| t.literal('open'), | ||
| t.literal('closed'), | ||
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | ||
| t.literal('acknowledged'), | ||
| ]), | ||
| index: t.string, | ||
| ids: t.undefined, | ||
| query: t.union([t.object, t.string]), | ||
| }), | ||
| t.intersection([ | ||
| t.strict({ | ||
| index: t.string, | ||
| ids: t.array(t.string), | ||
| query: t.undefined, | ||
| }), | ||
| t.partial({ | ||
| status: t.union([ | ||
| t.literal('open'), | ||
| t.literal('closed'), | ||
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | ||
| t.literal('acknowledged'), | ||
| ]), | ||
| addTags: t.array(t.string), | ||
| removeTags: t.array(t.string), | ||
| }), | ||
| ]), | ||
| t.intersection([ | ||
| t.strict({ | ||
| index: t.string, | ||
| ids: t.undefined, | ||
| query: t.union([t.object, t.string]), | ||
| }), | ||
| t.partial({ | ||
| status: t.union([ | ||
| t.literal('open'), | ||
| t.literal('closed'), | ||
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | ||
| t.literal('acknowledged'), | ||
| ]), | ||
| addTags: t.array(t.string), | ||
| removeTags: t.array(t.string), | ||
| }), | ||
| ]), | ||
| ]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few comments about this change 😅
1 - I am a bit rusty with io-ts, but how about the following to eliminate duplication?
| t.union([ | |
| t.strict({ | |
| status: t.union([ | |
| t.literal('open'), | |
| t.literal('closed'), | |
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | |
| t.literal('acknowledged'), | |
| ]), | |
| index: t.string, | |
| ids: t.array(t.string), | |
| query: t.undefined, | |
| }), | |
| t.strict({ | |
| status: t.union([ | |
| t.literal('open'), | |
| t.literal('closed'), | |
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | |
| t.literal('acknowledged'), | |
| ]), | |
| index: t.string, | |
| ids: t.undefined, | |
| query: t.union([t.object, t.string]), | |
| }), | |
| t.intersection([ | |
| t.strict({ | |
| index: t.string, | |
| ids: t.array(t.string), | |
| query: t.undefined, | |
| }), | |
| t.partial({ | |
| status: t.union([ | |
| t.literal('open'), | |
| t.literal('closed'), | |
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | |
| t.literal('acknowledged'), | |
| ]), | |
| addTags: t.array(t.string), | |
| removeTags: t.array(t.string), | |
| }), | |
| ]), | |
| t.intersection([ | |
| t.strict({ | |
| index: t.string, | |
| ids: t.undefined, | |
| query: t.union([t.object, t.string]), | |
| }), | |
| t.partial({ | |
| status: t.union([ | |
| t.literal('open'), | |
| t.literal('closed'), | |
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | |
| t.literal('acknowledged'), | |
| ]), | |
| addTags: t.array(t.string), | |
| removeTags: t.array(t.string), | |
| }), | |
| ]), | |
| ]) | |
| t.intersection([ | |
| t.exact( | |
| t.type({ | |
| index: t.string, | |
| }) | |
| ), | |
| t.union([ | |
| t.exact( | |
| t.type({ | |
| ids: t.array(t.string), | |
| query: t.undefined, | |
| }) | |
| ), | |
| t.exact( | |
| t.type({ | |
| ids: t.undefined, | |
| query: t.union([t.object, t.string]), | |
| }) | |
| ), | |
| ]), | |
| t.partial({ | |
| status: t.union([ | |
| t.literal('open'), | |
| t.literal('closed'), | |
| t.literal('in-progress'), // TODO: remove after migration to acknowledged | |
| t.literal('acknowledged'), | |
| ]), | |
| tags: t.array(t.string), | |
| }), | |
| ]) |
2 - Could you please add some tests to validate this? Since there isn't a dedicated schema file, we could do mock calls to this route and make sure all combinations of fields work as expected.
3 - I don't think we should have addTags and removeTags. (I changed it to just tags in the schema.)
A single tags field leads to more predictable behaviour from the api and I think will make our lives much easier. What is passed is what is saved, and we can validate that field immediately. The user also does not need to know the current state to make the updates they need.
With addTags and removeTags, off the top of my head:
- We need to always fetch the SO first to calculate the new list
- We need to pick an order to apply these. Do we add or remove first?
- We need to validate before applying
- We need to validate again after applying
- If using Terraform, the update operation would prob not work(how to calculate add and remove if someone just edits the tf file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
14a50bf to
1ee40ce
Compare
Co-Authored-By: Copilot <[email protected]>
1ee40ce to
f1b0628
Compare
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]
History
|
Closes #240356 Replaces [this PR](#241051) ## Summary This pull request introduces a new API route and supporting backend logic for bulk updating tags on alerts in the rule registry. The main changes include the addition of a `patchTags` method to the `AlertsClient`, a new route for bulk patching alert tags, and reusable scripts for tag updates. These improvements make it possible to add/remove or replace tags on multiple alerts efficiently, either by IDs or by query. ### Alerting authorization The `AlertingAuthorization` class changed to support bulk authorized multiple rule type IDs and consumers. The `ensureAuthorized` was renamed to `_ensureAuthorized` and accepts `Array<{ ruleTypeId: string; consumers: string[] }>;`. The logic is the same as before, but it constructs all security actions based on the input. This is needed to avoid having to do one authorization call per `(ruleTypeId, consumer)` pair. Lastly, a `bulkEnsureAuthorized` is exposed which is a wrapper of the private `_ensureAuthorized`. ### Alerts client The code in the `AlertsClient` is pretty outdated. For this reason, I decided not to use the existing functionality and code from scratch. The `bulkUpdateTags` is introduced, and it bulk updates the tags of multiple alerts either by using `alertIds` or by using a KQL `query`. In the first scenario, an aggregation is made to get the rule type ID and the consumer of each alert. Then we bulk authorize. If the user has access, we move forward and update the tags of the alerts. If not, we throw an error. For the `query` scenario, we apply the authorization filter along with the `query` to filter out the alerts that the user does not have access to. Lastly, we audit log only once for the whole bulk operation and not for each alert found. ### API and Backend Enhancements * Added a new API route `POST /internal/rac/alerts/tags` for bulk updating alert tags, supporting add, remove, and replace operations, with validation and error handling. [[1]](diffhunk://#diff-00a4668b8046bb9c2d423b91818e04ee9532682eecf426e5a80bce35276b0bd8R1-R113) [[2]](diffhunk://#diff-0abcdbe7de6b3dc00d522a6673ff7bcd99d0f4bf2d6002c59d475e85747d0970R19-R26) * Implemented the `bulkUpdateTags` method in `AlertsClient`, enabling tag updates on alerts by IDs or query, using Elasticsearch scripts for efficient bulk operations. ### Reusable Update Scripts * Added reusable Painless scripts for adding, removing, and replacing alert tags, exported from `alert_client_bulk_update_scripts.ts` and integrated into the client logic. [[1]](diffhunk://#diff-7c530f4d6aa0a6b9f0ca63714468130690bc5fa7e33a91591849318c1f380f7dR1-R34) [[2]](diffhunk://#diff-bbccd80f85af0d00f6eafdbc9e444164e6fee357697dc7be42e978fd376a357cR74-R78) --------- Co-authored-by: Elastic Machine <[email protected]> Co-authored-by: Christos Nasikas <[email protected]> Co-authored-by: kibanamachine <[email protected]>
Summary
This PR enhances the existing bulk update api for alerts to: