This note comes from a question on the OpenSearch forum: alert on any log line whose message contains one of ten words (error, warning, alert, failed, unavailable, lost, wrong, unable, invalid, failure), except lines containing six exclusion strings and lines from the sshd-session service, with one notification for every matching line. An earlier reply in that thread, from this author, suggested a per query monitor because “you get one alert per run instead of one per matching line.” For this goal that was the wrong pick, and the table below shows why. We measured all three monitor types instead of guessing.
How to check your monitor
Both checks are read-only. Set these first:
IDX=http://localhost:9200 # your cluster. Ours had the security plugin off: plain HTTP, no login.
CURL="curl -s" # with security on: CURL="curl -sk -u admin:YOUR_PASSWORD" and IDX=https://...
1. Which monitor type, and which execution policy on each action
$CURL "$IDX/_plugins/_alerting/monitors/MONITOR_ID" | python3 -m json.tool \
| grep -E '"monitor_type"|"interval"|"unit"|per_alert|per_execution|actionable_alerts|throttle_enabled'
query_level_monitor is a per query monitor, bucket_level_monitor per bucket, doc_level_monitor per document. If yours is query_level_monitor and you want one notification per line, that is the cause.
2. How many alerts, and for which documents
$CURL "$IDX/_plugins/_alerting/monitors/alerts?size=200" | python3 -c '
import json,sys
for a in json.load(sys.stdin).get("alerts",[]):
print(a["monitor_name"], a["id"], a["state"], a["start_time"], a.get("related_doc_ids"))'
In our test the per document monitor had one ACTIVE alert per matching document, each with one entry in related_doc_ids. The per query monitor kept no alert per document: it reports on the search as a whole.
What we measured
OpenSearch 3.6.0 (the indexer bundled with Wazuh 5.0.0-rc1, Alerting and Notifications plugins as shipped there), one node, security plugin off, our test containers, 28 October 2026. Times are UTC. A test index with message as text (plus a message.keyword subfield), service and host as keyword, and @timestamp as date. Three monitors, all created through POST /_plugins/_alerting/monitors, all on a 1-minute schedule, each with one webhook action to the same listener, "throttle_enabled": false.
Run 1: three matching lines, three monitor types
Three documents containing “error”, written at 04:04:07.7, 04:04:12.9 and 04:04:18.0, plus one control document with none of the words. All three monitors ran at 04:04:52. We counted what the webhook listener received, then kept watching until 04:07:46.
| monitor | how it was set up | notifications | alerts listed at 04:07:46 |
|---|---|---|---|
per query (query_level_monitor) | search over the last minute, match message: error; trigger ctx.results[0].hits.total.value > 0; action with no execution policy | 1 | 0 |
per document (doc_level_monitor) | doc-level query message:error; trigger query[name=q-error]; action per_alert, actionable_alerts: ["NEW"] | 3 | 3 ACTIVE, one per document, each with that document's id |
per bucket (bucket_level_monitor) | same search as per query, terms on message.keyword; trigger _count > 0; action per_alert, ["NEW"] | 3 | 0 |
- No repeats. From 04:04:52 to 04:07:46, about 3 minutes and at least 2 more runs, no monitor sent anything again. The control document produced nothing.
- Per bucket gave 3 only because the three lines had different text. It groups by the exact message, so two lines with identical text should share one bucket. We did not measure that case, so for “one notification per line” per document is the type that matches the goal by design.
Run 2: the full condition, six documents
One per document monitor with the whole condition as its doc-level query (below, in how to set it up), the same per_alert / ["NEW"] action. Six documents, 5 seconds apart, from 04:10:27 to 04:10:53:
| # | service | message | should notify? | notified? |
|---|---|---|---|---|
| 1 | pvedaemon | error starting VM 105 | yes | yes |
| 2 | kernel | ata2: link is slow to respond, unable to reset | yes | yes |
| 3 | pveproxy | proxy detected vanished client connection, request failed | yes | yes |
| 4 | pvedaemon | error: disabling inotify for this host | no (exclusion string) | no |
| 5 | sshd-session | pam_unix: authentication failed for user root | no (excluded service) | no |
| 6 | pvestatd | status update time 0.2 seconds | no (no keyword) | no |
The run at 04:11:12 sent exactly 3 notifications, and the 3 alerts' related_doc_ids were exactly documents 1, 2 and 3. Nothing more was sent through 04:14:20.
Same run on stock OpenSearch 3.8.0. We repeated run 2 on the opensearchproject/opensearch:3.8.0 image (Alerting 3.8.0.0, security plugin off), same index mapping, same query and action, same six documents from 04:58:19 to 04:58:45. The run at 04:59:04 sent exactly 3 notifications, the 3 alerts' related_doc_ids were documents 1, 2 and 3, and nothing more was sent through 05:02:13. We did not repeat run 1 (the per query and per bucket comparison) on 3.8.0.
Why it happens
A per query monitor runs your search once per run and evaluates the trigger once, on the whole result. Three hits or three hundred, the trigger is either true or false for that run. In our test the run that saw all three lines sent one notification from an action with no execution policy. That is useful when you want one message per run, which is what the earlier forum reply optimised for, and the wrong shape when you want one message per line.
A per document monitor checks each new document against its queries and keeps an alert per matching document. With the action on per_alert and actionable_alerts: ["NEW"], each new alert sent one notification, and none was sent again in the 3 minutes we watched: 3 documents, 3 alerts, 3 notifications, 0 repeats.
One thing we did not expect to be true, so we checked it: the API accepted per_alert with ["NEW"] on a per query monitor too, and stored it. We did not run that monitor, so we cannot tell you what it does in practice, and we did not check what the Dashboards editor offers for a per query monitor.
How to set it up (tested)
The per document monitor from run 2, with the whole condition in one doc-level query. Names and the message template differ from our test run; the query and the action policy are the ones we ran. Replace the index, the channel id and the message template with yours:
$CURL -XPOST "$IDX/_plugins/_alerting/monitors" -H 'Content-Type: application/json' -d '{
"type": "monitor",
"monitor_type": "doc_level_monitor",
"name": "critical-lines",
"enabled": true,
"schedule": { "period": { "interval": 1, "unit": "MINUTES" } },
"inputs": [ { "doc_level_input": {
"description": "",
"indices": [ "YOUR-INDEX" ],
"queries": [ {
"id": "q-critical", "name": "q-critical", "tags": [],
"query": "message:(error OR warning OR alert OR failed OR unavailable OR lost OR wrong OR unable OR invalid OR failure) AND NOT message:\"disabling inotify\" AND NOT message:\"user config - ignore\" AND NOT message:\"_put_session\" AND NOT message:\"PVE ticket\" AND NOT message:\"guest-ping\" AND NOT message:\"Cyberprotect\" AND NOT service:\"sshd-session\""
} ]
} } ],
"triggers": [ { "document_level_trigger": {
"name": "critical-line",
"severity": "1",
"condition": { "script": { "source": "query[name=q-critical]", "lang": "painless" } },
"actions": [ {
"name": "notify",
"destination_id": "YOUR-CHANNEL-ID",
"subject_template": { "source": "Critical log line", "lang": "mustache" },
"message_template": { "source": "Critical log line, run ending {{ctx.periodEnd}}", "lang": "mustache" },
"throttle_enabled": false,
"action_execution_policy": { "action_execution_scope": { "per_alert": { "actionable_alerts": [ "NEW" ] } } }
} ]
} } ]
}'
- The trigger condition names the query:
query[name=q-critical]. Doc-level queries use query string syntax, so the exclusion list is one string withAND NOTclauses. - Check your
servicefield. In our test indexservicewas a keyword field, soservice:"sshd-session"matched it exactly. If yours is text, test that the clause still excludes those lines, or point it at the keyword subfield. - Only two of the six exclusions were tested with a real line: “disabling inotify” and the
sshd-sessionservice. For the other four strings we only know the query was accepted. Send one line for each before you rely on it. - If your channel is a webhook to a private address, notifications can fail silently: the default
host_deny_listrefuses private ranges. We cleared it in the lab for this test and restored it afterwards. See webhooks to private addresses; allow only the address you need.
Limits of what we measured
OpenSearch 3.6.0, the indexer bundled with Wazuh 5.0.0-rc1, one node, security plugin off, plain HTTP, in our test containers. Run 2 alone was repeated on stock OpenSearch 3.8.0 (one node, security plugin off) with the same result; run 1 and the per query probe were not. Not a cluster. Monitors were created through the REST API, not the Dashboards editor. One webhook listener; we did not test email, Slack, Telegram or other channel types. Throttle was off. Two runs: 3 matching documents plus a control, then 6 documents. We watched for repeats for about 3 minutes after each; the 3 alerts were still ACTIVE at the end, and we did not follow them until they were acknowledged or completed. Not measured: per bucket with identical message text, a per query monitor actually running with per_alert, four of the six exclusion strings (accepted, no test line), a service field mapped as text, a message field holding an array of strings, and heavy load.