Skip to content

Fix/ppl lookback preview alert disable - #1501

Merged
lezzago merged 5 commits into
opensearch-project:mainfrom
riysaxen-amzn:fix/ppl-lookback-preview-alert-disable
Aug 20, 2026
Merged

Fix/ppl lookback preview alert disable#1501
lezzago merged 5 commits into
opensearch-project:mainfrom
riysaxen-amzn:fix/ppl-lookback-preview-alert-disable

Conversation

@riysaxen-amzn

Copy link
Copy Markdown
Collaborator

Description

Bug 1: Lookback window injects a frozen absolute time window.

addTimeFilterToQuery built the lookback filter from absolute timestamps computed at save time:

| where @timestamp > TIMESTAMP('2026-08-19 20:00:00') and @timestamp < TIMESTAMP('2026-08-19 21:00:00')

Since the filter is persisted into the monitor's query, every scheduled execution forever re-queries the same static window from the moment the monitor was saved — after the first interval elapses, the monitor can never match new data again.

Fix: inject a dynamic sliding window instead, evaluated by the engine at each execution:

| where @timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)

The interval unit is derived from the configured window (MINUTE/HOUR/DAY). All call sites (save serialization plus the query/trigger previews) share the same helper, so they inherit the fix.

Bug 2: Query preview broken on monitor edit.

Two independent causes:

  1. Stacked/stale time filters. On edit, the hydrated query already contains the previously injected lookback filter. Preview and re-save injected a second filter on top. Combined with Bug 1, the old filter's upper bound is entirely in the past, so preview on edit deterministically returned zero rows while create worked.
    Fix: addTimeFilterToQuery is now idempotent — a new stripTimeFilterFromQuery removes any previously injected filter for the timestamp field (both the new DATE_SUB form and the legacy absolute TIMESTAMP('...') form persisted by older saves) before injecting, so injection replaces rather than stacks. This also transparently migrates monitors saved with the old frozen filter the next time they are saved.

  2. dataSourceId dropped on edit. The edit branch of getInitialValues rebuilds the formik values wholesale from the stored monitor, discarding the query-param initialization — and the stored monitor carries no dataSourceId. Preview and field-detection calls then fall back to whatever data source the page happens to have selected, which in MDS environments may not be the monitor's data source.
    Fix: preserve dataSourceId from the edit-page URL when hydrating a monitor for edit (both the PPL and default hydration paths).

Bug 3: Monitors cannot be disabled from the Alerts page.

The Alerts page (DashboardClassic) offered only Acknowledge / View alert details / View detector — there was no way to stop a noisy monitor without navigating to the Monitors page and finding it by name.

Fix: add a Disable monitors action to the Alerts page toolbar. It collects the unique monitor_ids behind the selected alerts (skipping chained/workflow alerts, for which the button stays disabled) and disables each via the same fetch-then-PUT round-trip the Monitors page uses — the full monitor body is fetched and written back with only enabled: false changed, so all monitor types (including PPL) are handled safely. Sequence-number concurrency params and dataSourceId are forwarded, successes surface a toast, failures surface per-monitor error notifications, and the dashboard refreshes.

Testing

  • New/updated unit tests in public/pages/CreateMonitor/containers/CreateMonitor/utils/pplAlertingHelpers.test.js (9 tests for the time-filter helper): sliding DATE_SUB form with MINUTE/HOUR/DAY units, injection before the first pipe, preservation of complex piped queries, idempotence (re-injection replaces instead of stacking), replacement of the legacy absolute TIMESTAMP('...') filter, and non-interference with user-written filters on other fields.
  • Manually verified against OpenSearch 3.7 with the security plugin:
    • Lookback checkbox injects | where @timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR) exactly once (network capture of the preview request), and the saved monitor persists the same form.
    • Preview on edit sends exactly one time filter; re-saving an edited monitor does not stack clauses; a hand-written DATE_SUB filter on the same field is replaced, not duplicated.
    • Sliding-window boundary: docs older than the window are excluded at 1-hour lookback and included at 1-day lookback, with no change to the stored query.
    • Alerts page: selecting an alert enables the new Disable monitors button; clicking it disables the backing monitor (success toast + monitor shows Disabled on the Monitors page).

Related Issues

n/a — found while investigating PPL monitor reliability issues, follow-up to #1500.

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • Commits are signed per the DCO using --signoff.

…make injection idempotent

Signed-off-by: Riya Saxena <riysaxen@amazon.com>
Signed-off-by: Riya Saxena <riysaxen@amazon.com>
Signed-off-by: Riya Saxena <riysaxen@amazon.com>
…e disable action

Signed-off-by: Riya Saxena <riysaxen@amazon.com>
@riysaxen-amzn

Copy link
Copy Markdown
Collaborator Author

The three UT runs failing:

  • macOS "fail" verdict: 13 failed suites, all the @elastic/charts/node_modules/uuid parse breakage on main (suite count went 134 → 136 passed, the +2 being our new files)
  • Windows: The operation was canceled — fail-fast kill triggered by macOS finishing red first; zero test results, not a regression
  • Container "Run unit tests": bash: yarn: command not found, exit 127 before any tests — the broken CI image, and "Run binary installation" is the unpinned-actions policy failure affecting every PR

@lezzago lezzago left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review findings

Ranked most-severe first.

1. Stale time filter left baked into query when lookback disabled (High)

public/pages/CreateMonitor/containers/CreateMonitor/utils/pplAlertingMonitorToFormik.js:40

On edit, pplQuery is hydrated with the previously injected time filter still embedded. The strip logic (stripTimeFilterFromQuery) only runs inside addTimeFilterToQuery, which pplFormikToMonitor.js:207 skips whenever lbMinutes <= 0.

Failure scenario: Edit a PPL monitor saved with lookback ON (stored query source=logs | where @timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR) | stats count()), uncheck the lookback window, and save. pplFormikToMonitor.js sees lbMinutes=0 and skips addTimeFilterToQuery, so stripTimeFilterFromQuery is never called; the persisted query still contains | where @timestamp > DATE_SUB(...). The monitor keeps filtering by the window the user just disabled, and the raw filter is shown verbatim in the query editor. Fix: strip during hydration, or strip unconditionally on save.

2. Silently swallowed error in disableSelectedMonitors (Medium)

public/pages/Dashboard/containers/DashboardClassic.js:364

The per-monitor catch block returns the error object without emitting any notification. err?.ok is undefined, so a thrown/network failure isn't counted as success and no error toast fires.

Failure scenario: disableSelectedMonitors runs; the httpClient.get or .put for a monitor throws (network error / timeout). The catch returns err, which isn't counted as success and fires no backendErrorNotification. The user selects a monitor, clicks Disable monitors, the monitor is NOT disabled, and no feedback appears — contradicting the PR's stated "failures surface per-monitor error notifications."

3. Unescaped $ in string replace injection (Low)

public/pages/CreateMonitor/containers/CreateMonitor/utils/pplAlertingHelpers.js:191

Injection uses cleanQuery.replace('|', \${timeFilterClause} |`). If timestampFieldever contains a$, the replacement string's $-patterns (e.g. $&, $1) are interpreted rather than inserted literally, corrupting the injected clause. Low likelihood for typical field names, but the replacement is not escaped — a function replacer or split/join` would avoid it.

4. Redundant second queryString.parse (Low / maintainability)

public/pages/CreateMonitor/containers/CreateMonitor/utils/helpers.js:12

location.search is parsed a second time here solely to pull dataSourceId, duplicating the queryString.parse already done at line 40. Reuse the top-level parse for a single source of truth for URL params.

5. Inline duplication of Monitors.updateMonitor round-trip (Low / drift risk)

public/pages/Dashboard/containers/DashboardClassic.js:345

disableSelectedMonitors re-implements Monitors.updateMonitor's fetch-then-PUT (identical omit list, seqNo/primaryTerm/dataSourceId forwarding) inline. If the Monitors omit list changes later (e.g. a new server-managed field must be dropped), this copy will silently drift and PUT stale/invalid fields. Prefer extracting a shared disable-by-id helper rather than duplicating the body.


Generated by automated code review.

…date round-trip, surface thrown errors

Signed-off-by: Riya Saxena <riysaxen@amazon.com>
@riysaxen-amzn

Copy link
Copy Markdown
Collaborator Author

Code review findings

Ranked most-severe first.

1. Stale time filter left baked into query when lookback disabled (High)

public/pages/CreateMonitor/containers/CreateMonitor/utils/pplAlertingMonitorToFormik.js:40

On edit, pplQuery is hydrated with the previously injected time filter still embedded. The strip logic (stripTimeFilterFromQuery) only runs inside addTimeFilterToQuery, which pplFormikToMonitor.js:207 skips whenever lbMinutes <= 0.

Failure scenario: Edit a PPL monitor saved with lookback ON (stored query source=logs | where @timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR) | stats count()), uncheck the lookback window, and save. pplFormikToMonitor.js sees lbMinutes=0 and skips addTimeFilterToQuery, so stripTimeFilterFromQuery is never called; the persisted query still contains | where @timestamp > DATE_SUB(...). The monitor keeps filtering by the window the user just disabled, and the raw filter is shown verbatim in the query editor. Fix: strip during hydration, or strip unconditionally on save.

2. Silently swallowed error in disableSelectedMonitors (Medium)

public/pages/Dashboard/containers/DashboardClassic.js:364

The per-monitor catch block returns the error object without emitting any notification. err?.ok is undefined, so a thrown/network failure isn't counted as success and no error toast fires.

Failure scenario: disableSelectedMonitors runs; the httpClient.get or .put for a monitor throws (network error / timeout). The catch returns err, which isn't counted as success and fires no backendErrorNotification. The user selects a monitor, clicks Disable monitors, the monitor is NOT disabled, and no feedback appears — contradicting the PR's stated "failures surface per-monitor error notifications."

3. Unescaped $ in string replace injection (Low)

public/pages/CreateMonitor/containers/CreateMonitor/utils/pplAlertingHelpers.js:191

Injection uses cleanQuery.replace('|', \${timeFilterClause} |). If timestampFieldever contains a, t h e r e p l a c e m e n t s t r i n g ′ s-patterns (e.g. $&, $1) are interpreted rather than inserted literally, corrupting the injected clause. Low likelihood for typical field names, but the replacement is not escaped — a function replacer or split/join would avoid it.

4. Redundant second queryString.parse (Low / maintainability)

public/pages/CreateMonitor/containers/CreateMonitor/utils/helpers.js:12

location.search is parsed a second time here solely to pull dataSourceId, duplicating the queryString.parse already done at line 40. Reuse the top-level parse for a single source of truth for URL params.

5. Inline duplication of Monitors.updateMonitor round-trip (Low / drift risk)

public/pages/Dashboard/containers/DashboardClassic.js:345

disableSelectedMonitors re-implements Monitors.updateMonitor's fetch-then-PUT (identical omit list, seqNo/primaryTerm/dataSourceId forwarding) inline. If the Monitors omit list changes later (e.g. a new server-managed field must be dropped), this copy will silently drift and PUT stale/invalid fields. Prefer extracting a shared disable-by-id helper rather than duplicating the body.

Generated by automated code review.

All five addressed in 52c54e2: (1) buildPPLMonitorFromFormik now strips the injected filter on save when lookback is disabled (+ regression test); (2) thrown/network errors now surface backendErrorNotification (+ regression test); (3) injection uses a function replacer; (4) single queryString.parse in getInitialValues; (5) extracted fetchAndUpdateMonitor into public/utils/helpers.js and refactored both Monitors.updateMonitor and the new Alerts-page disable to delegate to it, so the round-trip can't drift.

@riysaxen-amzn
riysaxen-amzn requested a review from lezzago August 20, 2026 13:58
@lezzago
lezzago merged commit 667ebbd into opensearch-project:main Aug 20, 2026
14 of 17 checks passed
riysaxen-amzn added a commit that referenced this pull request Aug 27, 2026
…1505)

* Accept v1-shape monitor body in PPL update guard and toV1MonitorBody

The monitor details page enable/disable toggle round-trips the monitor in
raw v1 shape (query nested in inputs[0].ppl_input.query, nothing at top
level). toV1MonitorBody read only the top-level query and defaulted to '',
so before the empty-query guard this path silently wiped the monitor's PPL
query on every toggle; after the guard it was rejected outright.

Fall back to inputs[0].ppl_input.query in both toV1MonitorBody and the
updateMonitor guard so v1-shape bodies pass through with their query
intact, while truly query-less bodies are still rejected.

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* Address review: drop v1/v2 language, add empty nested-query rejection tests

- Rename toV1MonitorBody -> toEngineMonitorBody, flattenV1Monitor ->
  flattenEngineMonitor, and reword all v1/v2 comments to 'engine format'
  vs 'flattened format' (toepkerd) -- v2 APIs were never launched, so
  version language is a vestige of the removed v2 push.
- Add tests asserting the guard still rejects engine-shape bodies whose
  nested query is empty or whose ppl_input is empty (eirsep).

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* Fix pre-existing main test failure: unwrap ppl_monitor in lookback-strip test

buildPPLMonitorFromFormik returns the wrapped shape { ppl_monitor: {...} }
(from #1500), but the lookback-strip test (from #1501) asserted query on
the top-level return, which is undefined -- a semantic merge conflict
between the two PRs that has kept the unit-test workflow red on main
since Aug 20. Assert against ppl_monitor.query instead.

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

* Address review: strip stale trigger metadata from engine-shape bodies, extract shared extractPplQuery helper

- Unwrap ppl_trigger before stripping id/last_triggered_time/last_execution_time
  so the newly-accepted engine-shape bodies get the same trigger cleaning as
  the flattened path
- Extract extractPplQuery() shared by the update guard and toEngineMonitorBody
  so the two can never drift apart
- A whitespace-only top-level query no longer short-circuits the nested
  query fallback

Signed-off-by: Riya Saxena <riysaxen@amazon.com>

---------

Signed-off-by: Riya Saxena <riysaxen@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants