Alerting on ILM Errors with Kibana Stack Monitoring
Turn the ERROR-step signal that _ilm/explain exposes into an actual alert, because Elasticsearch ships no native rule that pages when a lifecycle stalls.
Kibana Stack Monitoring visualises cluster and index metrics ILM depends on, but it has no ILM-aware alerting rule — there is no checkbox that says “page me when an index gets stuck.” So alerting on lifecycle failures always means deriving a signal yourself and feeding it into a rule. This page shows the two ways to do that: a scheduled poller that scans _ilm/explain and emits a metric or log the alerting stack already understands, or a Kibana alerting rule evaluated over that derived signal. It sits alongside the read-only instrumentation in monitoring ILM health with the _cat and explain APIs and the manual remediation in manually triggering an ILM phase transition, all built on Index Lifecycle Management (ILM). The running example is the events-* pattern managed by the events-ilm policy.
What you page on matters as much as how. Three signals are worth waking someone for: any index whose step equals ERROR, the ILM service reporting anything other than RUNNING, and any data tier over its high disk watermark. The trap to avoid is paging on ordinary in-progress steps — an index sitting in check-rollover-ready or allocate is not broken, it is working, and an alert that fires on those trains everyone to ignore it.
Prerequisites
- Elasticsearch 8.x with the
events-ilmpolicy managing theevents-*indices. elasticsearch-pyv8.0+ for the poller (ilm.explain_lifecycle,ilm.get_status).- A monitoring account with
read_ilmandmonitor— the poller only reads; it never remediates. - A sink the poller can emit to (a metrics backend, a log index Kibana can alert over, or a scheduler webhook) plus a Kibana alerting connector if you route through Kibana rules.
Implementation
1. A scheduled poller that emits a stuck-index signal
The poller scans the managed pattern each run, keeps only the genuinely halted indices, checks the global service switch, and returns a compact payload the sink can threshold. It emits a count and the offending index names, so a rule can fire on stuck_count > 0.
import json
import logging
from elasticsearch import Elasticsearch, ApiError
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("ilm_alert")
def collect_ilm_alert_signal(es: Elasticsearch, pattern: str = "events-*") -> dict:
"""Return an alertable payload: stuck indices, service mode, and a page flag."""
payload = {"stuck_count": 0, "stuck": [], "service_mode": "RUNNING", "should_page": False}
# Global switch first: a non-RUNNING engine freezes the whole fleet.
status = es.ilm.get_status()
payload["service_mode"] = status["operation_mode"]
try:
resp = es.ilm.explain_lifecycle(index=pattern, human=True)
except ApiError as exc:
logger.error("explain failed (%s): %s", exc.status_code, exc.info)
payload["scan_failed"] = True
payload["should_page"] = True # inability to scan is itself alertable
return payload
for idx, meta in resp["indices"].items():
if not meta.get("managed"):
continue
# ONLY the ERROR step is a stall. Waiting steps are healthy and must not page.
if meta.get("step") == "ERROR":
payload["stuck"].append({
"index": idx,
"phase": meta.get("phase"),
"failed_step": meta.get("failed_step"),
"reason": (meta.get("step_info") or {}).get("reason"),
})
payload["stuck_count"] = len(payload["stuck"])
payload["should_page"] = (
payload["stuck_count"] > 0 or payload["service_mode"] != "RUNNING"
)
return payload
def emit(payload: dict) -> None:
"""Sketch of wiring to a sink: write one structured record the alert rule reads.
Replace the body with your transport — index into a monitoring index, push a
gauge to a metrics backend, or POST to a scheduler webhook. Keep it one record
per run so the rule can threshold on stuck_count and dedupe on index name.
"""
logger.info("ilm_alert_signal %s", json.dumps(payload, sort_keys=True))
if __name__ == "__main__":
es = Elasticsearch("https://es-monitor-01:9200",
api_key="YOUR_READ_ILM_MONITOR_API_KEY", verify_certs=True)
signal = collect_ilm_alert_signal(es, "events-*")
emit(signal)
if signal["should_page"]:
logger.warning("PAGE: %d stuck, service=%s",
signal["stuck_count"], signal["service_mode"])Run it on a scheduler (cron, a Kubernetes CronJob, a Watcher-adjacent job) at roughly the ILM poll interval. The emit function is deliberately a stub: point it at whatever your team already alerts over. If you index the payload into a monitoring index, a Kibana alerting rule of the form “documents where stuck_count > 0 in the last interval” gives you the page without any custom code — the poller does the ILM-specific classification and Kibana does the notification.
2. Routing through a Kibana alerting rule
If you would rather keep the logic in Kibana, index the poller’s per-index stuck records into a dedicated index (for example ilm-health-signals) and build an alerting rule over it: group by index, condition on the presence of an ERROR record in the last window, and connect it to your notification channel. The poller still does the classification — Kibana Stack Monitoring cannot read the ERROR step on its own — but the rule engine, deduplication, and connectors come for free.
Verification
Prove the alert fires by forcing a benign, reversible ERROR in staging rather than waiting for a real one. The cleanest way is to point an index’s rollover alias at the wrong target, which makes the check-rollover-ready step fail into ERROR:
POST _ilm/move/events-000002
{
"current_step": { "phase": "hot", "action": "rollover", "name": "check-rollover-ready" },
"next_step": { "phase": "hot", "action": "rollover", "name": "check-rollover-ready" }
}More reliably, temporarily stop the service and confirm the service-mode branch pages:
POST _ilm/stopWithin one poll cycle, collect_ilm_alert_signal should return service_mode: "STOPPED" and should_page: true. Confirm the emitted record shows it:
GET _ilm/status{ "operation_mode": "STOPPED" }Once the alert has fired and you have confirmed the page arrived, restore normal operation so nothing stays frozen:
POST _ilm/startRe-run the poller and confirm should_page returns to false with service_mode: "RUNNING" and an empty stuck list — proving the alert clears as well as fires.
Gotchas and edge cases
- Poll interval versus
poll_interval. ILM only re-evaluates the state machine everyindices.lifecycle.poll_interval(default10m). Scheduling the alert poller far faster does not surface stalls any sooner — it just re-reports identical steps and adds master-node load. Match the poller to the ILM interval; a stall becomes visible one ILM cycle after it happens regardless of how often you poll. - Transient waiting steps are not errors — never page on them.
check-rollover-ready,allocate,shrink, andforcemergeare in-progress states an index legitimately occupies for minutes to hours. The poller must filter tostep == "ERROR"only. Alerting on any non-completestep produces constant false pages and destroys trust in the signal. - Dedupe flapping. An index can briefly enter
ERROR, get retried by ILM, and recover, and a naive rule pages on every flap. Require theERRORto persist across two consecutive poll windows before paging, and group notifications by index name so one stuck index is one incident, not one page per poll. - Scope by managed pattern. Alert over
events-*, not*. A wide pattern drags unmanaged and system indices into the scan, inflates the master-node cost of each_ilm/explaincall, and risks paging on indices no one owns. Keep the pattern tight to the lifecycle you actually run.
FAQ
Does Kibana Stack Monitoring alert on stuck ILM indices out of the box?
No. Stack Monitoring charts cluster and index metrics ILM runs on, but it has no rule that reads the lifecycle ERROR step. You derive the signal yourself — a scheduled poller that scans _ilm/explain and emits a metric or log — and then alert on that derived signal, either with a Kibana alerting rule over the emitted records or with your existing metrics-backend alerting.
Which conditions actually deserve a page?
Three: any index whose step equals ERROR (a genuine stall), _ilm/status reporting anything other than RUNNING (the whole engine is frozen), and any data tier over its high disk watermark (the usual upstream cause of allocate stalls). Rollover lag — a write index past its rollover condition that has not rolled — is worth a warning. In-progress steps like check-rollover-ready are never a page.
How do I stop the alert from flapping?
Require the ERROR to persist across two consecutive poll windows before paging, and deduplicate by index name so a single stuck index is one incident rather than one page per poll. ILM retries some failures on its own, so a one-shot ERROR that clears on the next cycle should resolve silently; only a stall that survives multiple polls warrants waking someone.
Related
- Manually Triggering an ILM Phase Transition — the remediation runbook once an alert fires.
- Monitoring ILM Health with the _cat and Explain APIs — the full explain-and-correlate loop this alerting derives from.
← Back to Monitoring ILM Health · ILM Architecture & Fundamentals