- Posted on
- • Artificial Intelligence
AI Agents for Monitoring
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
AI Agents for Monitoring: a Bash-first playbook for smarter, quieter ops
If your on-call rotation still feels like a slot machine of noisy alerts, you’re not alone. Classic monitoring stacks are great at collecting numbers and firing thresholds; they’re less great at telling you what actually matters right now, or what to do about it. Enter AI agents for monitoring: small, composable helpers you can bolt onto your existing Linux stack to summarize, prioritize, and even auto-remediate common issues—without ripping out your current tooling.
This guide shows you how to stand up two tiny agents on any Linux box using Bash and Python:
A log triage agent that summarizes spikes and suggests likely causes
A metrics anomaly agent that learns baselines and flags outliers
Optional safe auto-remediation with allowlisted actions
Webhook notifications to your chat or incident system
You’ll get actionable, explainable output and fewer false positives—while keeping control and observability in your hands.
Why AI agents for monitoring are worth your time
Reduce noise, keep signal: Thresholds scream when a number crosses a line. Agents can group related errors, deduplicate repeats, and explain impact in plain language.
Faster triage: Summaries and suggested next steps shrink mean-time-to-understand, a big part of MTTR.
Start small, scale safely: You can pilot AI-assisted summarization locally with guardrails, then graduate to more automation as confidence grows.
Works with what you have: Journald, sysstat, cron/systemd timers, webhooks—no wholesale migration needed.
What you’ll build
triage_agent.py: batches recent logs, detects patterns (timeouts, OOMs, 5xx storms), and optionally calls an LLM endpoint for a short summary and classification.
metrics_agent.py: gathers a few host metrics, learns a rolling baseline, and flags robust anomalies using median/MAD.
actions.sh: optional allowlisted actions (e.g., safe service restarts) with dry-run.
notify.sh: ships JSON events to a webhook or syslog.
Everything runs as a dedicated system user with minimal privileges. You can expand from here: more detectors, more playbooks, deeper integrations.
Prerequisites and installation (apt, dnf, zypper)
You only need standard Linux tools. Install prerequisites using your package manager.
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y python3 python3-venv python3-pip jq curl git coreutils sysstat
- Fedora/RHEL/CentOS (dnf):
sudo dnf install -y python3 python3-virtualenv python3-pip jq curl git coreutils sysstat
- openSUSE/SLES (zypper):
sudo zypper refresh
sudo zypper install -y python3 python3-virtualenv python3-pip jq curl git coreutils sysstat
Create a dedicated user and directories:
sudo useradd --system --home /opt/ai-monitor --shell /usr/sbin/nologin ai-monitor || true
sudo mkdir -p /opt/ai-monitor /var/log/ai-monitor /var/lib/ai-monitor
sudo chown -R ai-monitor:ai-monitor /opt/ai-monitor /var/log/ai-monitor /var/lib/ai-monitor
Give the agent read access to the journal (varies by distro; add both if unsure):
# For journald access
sudo usermod -aG systemd-journal ai-monitor 2>/dev/null || true
# On some Debian/Ubuntu systems logs are readable to adm group
sudo usermod -aG adm ai-monitor 2>/dev/null || true
Optional: if you later want safe service restarts from the agent, give ai-monitor limited sudo for specific units (example for nginx):
echo 'ai-monitor ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx' | sudo tee /etc/sudoers.d/ai-monitor-restarts >/dev/null
sudo chmod 440 /etc/sudoers.d/ai-monitor-restarts
Step 1 — Directory layout
sudo -u ai-monitor bash -c '
cd /opt/ai-monitor
touch triage_agent.py metrics_agent.py actions.sh notify.sh
chmod +x actions.sh notify.sh
'
Step 2 — Log triage agent
This script reads log lines from stdin, groups and scores signals (errors, timeouts, OOM kills, HTTP 5xx bursts), and outputs a compact JSON incident with an optional LLM-powered summary when AI_ENDPOINT and AI_API_KEY are set. If no LLM is configured, it falls back to a rule-based summary.
Create /opt/ai-monitor/triage_agent.py:
#!/usr/bin/env python3
import os, sys, re, json, time, socket, subprocess, urllib.request
MAX_LINES = int(os.getenv("TRIAGE_MAX_LINES", "800"))
BATCH_SECONDS = int(os.getenv("TRIAGE_BATCH_SECONDS", "60"))
HOST = socket.gethostname()
# Simple signal detectors
PATTERNS = {
"oom_kill": re.compile(r"Out of memory|Killed process \d+|oom-killer", re.I),
"timeout": re.compile(r"timeout|timed out|deadline exceeded", re.I),
"conn_reset": re.compile(r"connection reset|broken pipe", re.I),
"auth_fail": re.compile(r"failed password|authentication failure", re.I),
"segfault": re.compile(r"segfault|segmentation fault", re.I),
"http_5xx": re.compile(r"\s5\d{2}\s"),
"db_error": re.compile(r"(deadlock|too many connections|lock wait timeout|read-only)* (mysql|postgres|pg|sqlite)", re.I),
}
def parse_program(line):
# Try "prog[pid]:" or "prog:" patterns
m = re.search(r"\s([a-zA-Z0-9_\-\.]+)(?:\[\d+\])?:", line)
return m.group(1) if m else "unknown"
def score_lines(lines):
counts = {k:0 for k in PATTERNS}
by_prog = {}
errs = 0
for ln in lines:
prog = parse_program(ln)
by_prog[prog] = by_prog.get(prog, 0) + 1
if re.search(r"\b(error|fail|crit|alert|panic)\b", ln, re.I):
errs += 1
for k, rx in PATTERNS.items():
if rx.search(ln):
counts[k] += 1
top_prog = sorted(by_prog.items(), key=lambda x: x[1], reverse=True)[:3]
return counts, top_prog, errs
def heuristic_summary(counts, top_prog, total):
signals = sorted([(v,k) for k,v in counts.items() if v>0], reverse=True)
if not signals and total == 0:
return "No notable signals in this window.", "none", 0.1
top = [k for _,k in signals[:3]]
prog_str = ", ".join([f"{p}({c})" for p,c in top_prog]) if top_prog else "unknown"
if "oom_kill" in top:
return f"OOM activity detected; processes killed. Top programs: {prog_str}.", "resource_exhaustion", 0.8
if "http_5xx" in top and any(p for p,_ in top_prog if "nginx" in p or "apache" in p):
return f"HTTP 5xx surge likely from web tier. Top programs: {prog_str}.", "service_error", 0.7
if "timeout" in top or "conn_reset" in top:
return f"Network timeouts/connection resets observed. Top programs: {prog_str}.", "network_instability", 0.6
if "auth_fail" in top:
return f"Spike in authentication failures. Top programs: {prog_str}.", "security_auth", 0.6
if "segfault" in top:
return f"Segmentation faults seen. Top programs: {prog_str}.", "crash", 0.7
return f"Elevated errors across {prog_str}.", "generic_error", 0.5
def llm_summarize(text, default_summary, default_type, default_conf):
endpoint = os.getenv("AI_ENDPOINT", "").strip()
api_key = os.getenv("AI_API_KEY", "").strip()
model = os.getenv("AI_MODEL", "gpt-4o-mini")
if not endpoint or not api_key:
return default_summary, default_type, default_conf
try:
prompt = ("Summarize the root cause and impact in 2-3 sentences; return a JSON object with keys "
"'summary', 'incident_type' (one of: service_error, resource_exhaustion, network_instability, security_auth, crash, generic_error, none), "
"'confidence' (0..1), and 'suggested_action' (like restart_service:<name> or investigate:<hint>). "
"Logs:\n" + text[:12000])
req = {
"model": model,
"messages": [
{"role":"system", "content":"You are a reliable SRE assistant."},
{"role":"user", "content":prompt}
],
"temperature": 0.2
}
data = json.dumps(req).encode("utf-8")
r = urllib.request.Request(endpoint, data=data, headers={
"Content-Type":"application/json",
"Authorization": f"Bearer {api_key}"
})
with urllib.request.urlopen(r, timeout=20) as resp:
out = resp.read().decode("utf-8", "ignore")
# Try to extract JSON either from tool output or direct content
j = None
try:
obj = json.loads(out)
# OpenAI-compatible: choices[0].message.content
content = obj.get("choices",[{}])[0].get("message",{}).get("content","")
j = json.loads(content) if content.strip().startswith("{") else None
except Exception:
j = None
if j and isinstance(j, dict):
return j.get("summary", default_summary), j.get("incident_type", default_type), float(j.get("confidence", default_conf)), j.get("suggested_action","")
except Exception:
pass
return default_summary, default_type, default_conf, ""
def main():
start = time.time()
buf = []
for ln in sys.stdin:
buf.append(ln.rstrip("\n"))
if len(buf) >= MAX_LINES:
break
if time.time() - start >= BATCH_SECONDS:
break
total = len(buf)
counts, top_prog, errs = score_lines(buf)
default_summary, incident_type, confidence = heuristic_summary(counts, top_prog, total)
suggested_action = ""
joined = "\n".join(buf)
ret = llm_summarize(joined, default_summary, incident_type, confidence)
if isinstance(ret, tuple) and len(ret) == 4:
summary, incident_type, confidence, suggested_action = ret
else:
summary, incident_type, confidence = ret
event = {
"kind": "log_triage",
"host": HOST,
"total_lines": total,
"errors": errs,
"top_programs": [{"program":p,"count":c} for p,c in top_prog],
"counts": counts,
"incident_type": incident_type,
"confidence": round(confidence,2),
"summary": summary,
"suggested_action": suggested_action,
"ts": int(time.time())
}
print(json.dumps(event, ensure_ascii=False))
if __name__ == "__main__":
main()
How to run (ad-hoc):
# Last 2 minutes of logs into the triage agent
journalctl --since "2 min ago" --no-pager -o short | /usr/bin/python3 /opt/ai-monitor/triage_agent.py
Optional LLM config (use your endpoint/token; can be OpenAI-compatible or self-hosted):
export AI_ENDPOINT="https://api.openai.com/v1/chat/completions"
export AI_API_KEY="sk-..."
export AI_MODEL="gpt-4o-mini"
Tip: If you don’t set AI_ENDPOINT/AI_API_KEY, the heuristic summary still works.
Step 3 — Metrics anomaly agent (MAD-based, no external deps)
This script collects a few host metrics, learns baselines, and uses a robust z-score (Median Absolute Deviation) to flag spikes or drops. State is persisted under /var/lib/ai-monitor/state.json.
Create /opt/ai-monitor/metrics_agent.py:
#!/usr/bin/env python3
import os, sys, json, time, socket, shutil
from statistics import median
HOST = socket.gethostname()
STATE_FILE = os.getenv("AI_MONITOR_STATE", "/var/lib/ai-monitor/state.json")
WINDOW = int(os.getenv("AI_MONITOR_WINDOW", "288")) # ~24h if every 5m
THRESH = float(os.getenv("AI_MONITOR_Z", "6.0")) # robust z threshold
def read_mem_available():
avail = total = 0
with open("/proc/meminfo") as f:
for ln in f:
if ln.startswith("MemAvailable:"):
avail = int(ln.split()[1]) * 1024
if ln.startswith("MemTotal:"):
total = int(ln.split()[1]) * 1024
pct_free = (avail/total)*100 if total>0 else 0
return round(100-pct_free,2) # memory used percent
def read_load1():
with open("/proc/loadavg") as f:
return float(f.read().split()[0])
def read_disk_used_pct(path="/"):
t = shutil.disk_usage(path)
used = t.used / t.total * 100 if t.total>0 else 0
return round(used,2)
def robust_z(values, x):
if len(values) < 10:
return 0.0
m = median(values)
abs_dev = [abs(v - m) for v in values]
mad = median(abs_dev) or 1e-6
return 0.6745 * (x - m) / mad
def load_state():
try:
with open(STATE_FILE) as f:
return json.load(f)
except Exception:
return {"load1": [], "mem_used_pct": [], "disk_used_pct_root": []}
def save_state(st):
tmp = STATE_FILE + ".tmp"
with open(tmp, "w") as f:
json.dump(st, f)
os.replace(tmp, STATE_FILE)
def main():
st = load_state()
now = int(time.time())
metrics = {
"load1": read_load1(),
"mem_used_pct": read_mem_available(),
"disk_used_pct_root": read_disk_used_pct("/")
}
events = []
for k, val in metrics.items():
z = robust_z(st.get(k, []), val)
if abs(z) >= THRESH:
events.append({
"kind":"metric_anomaly",
"host": HOST,
"metric": k,
"value": val,
"robust_z": round(z,2),
"ts": now,
"summary": f"Anomalous {k}={val} (robust_z={z:.2f})",
"incident_type": "resource_exhaustion" if z>0 else "sudden_drop"
})
# update state
series = (st.get(k, []) + [val])[-WINDOW:]
st[k] = series
save_state(st)
for e in events:
print(json.dumps(e))
if __name__ == "__main__":
main()
Run ad-hoc:
/usr/bin/python3 /opt/ai-monitor/metrics_agent.py
Step 4 — Safe action runner and notifier
Only run allowlisted actions, default to dry-run, and log everything. Start conservative: suggest actions first, then enable automation.
Create /opt/ai-monitor/actions.sh:
#!/usr/bin/env bash
set -euo pipefail
# Comma-separated safelist: e.g. AI_MONITOR_ALLOW_RESTART="nginx,redis-server"
ALLOW_RESTART="${AI_MONITOR_ALLOW_RESTART:-}"
DRY_RUN="${AI_MONITOR_DRY_RUN:-1}" # 1 = simulate by default
is_allowed() {
local svc="$1"
[[ ",${ALLOW_RESTART}," == *",$svc,"* ]]
}
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Pass through non-JSON lines
if ! echo "$line" | jq . >/dev/null 2>&1; then
echo "$line"
continue
fi
kind=$(echo "$line" | jq -r '.kind // empty')
suggested=$(echo "$line" | jq -r '.suggested_action // empty')
host=$(echo "$line" | jq -r '.host // "unknown"')
if [[ "$kind" == "log_triage" && "$suggested" == restart_service:* ]]; then
svc="${suggested#restart_service:}"
if [[ -n "$svc" ]] && is_allowed "$svc"; then
if [[ "$DRY_RUN" == "1" ]]; then
echo "$line" | jq --arg a "Would restart $svc (dry-run)" '. + {action_taken:$a}'
else
# Use sudo if needed, constrain via sudoers
if sudo /usr/bin/systemctl restart "$svc"; then
echo "$line" | jq --arg a "Restarted $svc" '. + {action_taken:$a}'
else
echo "$line" | jq --arg a "Failed to restart $svc" '. + {action_error:$a}'
fi
fi
continue
fi
fi
# Default: no action
echo "$line" | jq '. + {action_taken:"none"}'
done
Create /opt/ai-monitor/notify.sh:
#!/usr/bin/env bash
set -euo pipefail
WEBHOOK="${MONITOR_WEBHOOK_URL:-}"
CHANNEL="${MONITOR_CHANNEL:-ai-monitor}"
HOST="$(hostname)"
post_webhook() {
local payload="$1"
curl -fsS -m 10 -H "Content-Type: application/json" -X POST -d "$payload" "$WEBHOOK" >/dev/null
}
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ -n "$WEBHOOK" ]]; then
# Wrap line as JSON if it's not valid JSON
if echo "$line" | jq . >/dev/null 2>&1; then
payload=$(jq -cn --arg ch "$CHANNEL" --arg h "$HOST" --argjson ev "$line" \
'{channel:$ch, host:$h, event:$ev}')
post_webhook "$payload" || logger -t ai-monitor "Webhook failed"
else
payload=$(jq -cn --arg ch "$CHANNEL" --arg h "$HOST" --arg msg "$line" \
'{channel:$ch, host:$h, message:$msg}')
post_webhook "$payload" || logger -t ai-monitor "Webhook failed"
fi
else
# Fallback to syslog
logger -t ai-monitor -- "$line"
fi
done
Environment knobs you can set (system-wide in /etc/default/ai-monitor or per-unit):
AI_MONITOR_DRY_RUN=1
AI_MONITOR_ALLOW_RESTART="nginx,redis-server"
MONITOR_WEBHOOK_URL="https://your.webhook/endpoint"
Step 5 — Wire it up with systemd timers
Create unit and timer files.
sudo tee /etc/systemd/system/ai-monitor-triage.service >/dev/null <<'EOF'
[Unit]
Description=AI log triage (batch recent logs)
After=network-online.target
[Service]
Type=oneshot
User=ai-monitor
Group=ai-monitor
Environment="AI_MONITOR_DRY_RUN=1"
# Optional LLM:
# Environment="AI_ENDPOINT=https://api.openai.com/v1/chat/completions"
# Environment="AI_API_KEY=sk-..."
# Environment="AI_MODEL=gpt-4o-mini"
# Allow service restarts (set sudoers if enabling) and safelist:
# Environment="AI_MONITOR_ALLOW_RESTART=nginx,redis-server"
# Notifications:
# Environment="MONITOR_WEBHOOK_URL=https://hooks.example.com/..."
ExecStart=/bin/bash -lc 'journalctl --since "2 min ago" --no-pager -o short \
| /usr/bin/python3 /opt/ai-monitor/triage_agent.py \
| /bin/bash /opt/ai-monitor/actions.sh \
| /bin/bash /opt/ai-monitor/notify.sh'
StandardOutput=journal
StandardError=journal
EOF
sudo tee /etc/systemd/system/ai-monitor-triage.timer >/dev/null <<'EOF'
[Unit]
Description=Run AI log triage every 2 minutes
[Timer]
OnCalendar=*:0/2
Persistent=true
[Install]
WantedBy=timers.target
EOF
Metrics anomaly every 5 minutes:
sudo tee /etc/systemd/system/ai-monitor-metrics.service >/dev/null <<'EOF'
[Unit]
Description=AI metrics anomaly agent
After=network-online.target
[Service]
Type=oneshot
User=ai-monitor
Group=ai-monitor
Environment="AI_MONITOR_WINDOW=288"
Environment="AI_MONITOR_Z=6.0"
ExecStart=/bin/bash -lc '/usr/bin/python3 /opt/ai-monitor/metrics_agent.py \
| /bin/bash /opt/ai-monitor/actions.sh \
| /bin/bash /opt/ai-monitor/notify.sh'
StandardOutput=journal
StandardError=journal
EOF
sudo tee /etc/systemd/system/ai-monitor-metrics.timer >/dev/null <<'EOF'
[Unit]
Description=Run AI metrics anomaly agent every 5 minutes
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
EOF
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now ai-monitor-triage.timer ai-monitor-metrics.timer
Check status and logs:
systemctl list-timers | grep ai-monitor
journalctl -u ai-monitor-triage.service -n 50 -e
journalctl -u ai-monitor-metrics.service -n 50 -e
Real-world style examples
Web tier error storm: Nginx starts returning 502/504 after a deploy. Triage agent groups the 5xx surge, flags probable service_error with nginx atop top_programs, and suggests restart_service:nginx. With DRY_RUN=1 you get a preview; add nginx to AI_MONITOR_ALLOW_RESTART to let the agent self-heal next time.
Sudden memory pressure: A runaway process fills memory. Metrics agent detects a high robust_z on mem_used_pct and emits an incident_type=resource_exhaustion. Your webhook receives a compact JSON; your runbook or on-call can act before OOM kills hit.
SSH brute-force: auth_fail spikes. The triage agent classifies security_auth, sending you a short summary rather than a hundred identical alerts.
Safety and operability tips
Start with DRY_RUN and summaries only. Flip to automation selectively for well-understood playbooks.
Whitelist exact actions and services. Avoid wildcards.
Log everything: inputs, decisions, and actions.
Rate-limit actions (e.g., only one restart per 30 minutes per service).
Least privilege: the ai-monitor user should only have the sudo commands it truly needs.
Treat the LLM as advisory: keep heuristics and guardrails in place even when summaries come from AI.
Your next step
Deploy the timers, watch a few cycles, and tune thresholds and allowlists.
Add detectors: disk IO latency (iostat), TCP retransmits (ss), app-specific logs.
Enrich notifications with runbook URLs and dashboards.
If you run an internal LLM, point AI_ENDPOINT at it for privacy and resilience.
AI agents don’t replace your monitoring—they make it calmer and more actionable. Start with the two tiny helpers above, and iterate towards a smarter, safer on-call.