feat: align Alertmanager notifications with the CrowdSec feed format
🚀 Docker Build and Push / build-and-push (push) Successful in 4m46s

Alertmanager notifications were hard to read: the alert name appeared
twice, the instance was repeated between the header and the summary, the
count was restated in the body, and details ran flat with no visual
structure.

They now follow the same grammar as the CrowdSec feed: a title carrying
the counts, then one block per alert made of an identity line
("<icon> <name> (<context>)") and details indented under a tree marker.
Severity moves into the icon (red/orange/blue, green once resolved)
instead of a "[critical]" suffix, which keeps the identity line short
enough to read at a glance on a phone.
This commit is contained in:
maxime.killinger committed 2026-08-31 01:04:19 +02:00
1 parent 0337eb3071
commit 3d5f3c6299
1 file changed
+48 -36
+48 -36
View File
@@ -61,6 +61,43 @@ def format_scenario(scenario):
return "unknown" return "unknown"
return scenario.replace("crowdsecurity/", "").replace("_", " ") return scenario.replace("crowdsecurity/", "").replace("_", " ")
# One icon per alert, mirroring the per-IP icon used by the CrowdSec feed.
# Severity is carried by the icon instead of a "[critical]" suffix, which
# keeps the identity line short enough to read at a glance on a phone.
SEVERITY_ICONS = {
"critical": "🔴",
"warning": "🟠",
"info": "🔵",
}
def format_alert_block(alert, resolved=False):
"""Render one alert the way the CrowdSec feed renders one IP: an identity
line (icon, name, context in parentheses), then the details indented under
a tree marker. Returns the block as a single string."""
labels = alert.get("labels", {})
annotations = alert.get("annotations", {})
name = labels.get("alertname", "alerte")
severity = labels.get("severity", "")
# "name" is what cAdvisor-based rules use to carry the container name
context = (labels.get("instance") or labels.get("container")
or labels.get("name") or labels.get("job") or "")
icon = "✅" if resolved else SEVERITY_ICONS.get(severity, "🔥")
head = f"{icon} {name}"
if context:
head += f" ({context})"
summary = annotations.get("summary")
description = annotations.get("description")
details = [summary] if summary else []
# description often just restates summary; only keep it when it adds something
if description and description != summary:
details.append(description)
lines = [head] + [f" └ {d}" for d in details]
return "\n".join(lines)
@app.route('/crowdsec', methods=['POST']) @app.route('/crowdsec', methods=['POST'])
def handle_crowdsec(): def handle_crowdsec():
try: try:
@@ -120,48 +157,23 @@ def handle_alertmanager():
logger.debug("Received empty Alertmanager payload") logger.debug("Received empty Alertmanager payload")
return jsonify({"status": "no data"}), 200 return jsonify({"status": "no data"}), 200
def format_alert(alert):
labels = alert.get("labels", {})
annotations = alert.get("annotations", {})
name = labels.get("alertname", "alerte")
severity = labels.get("severity", "")
instance = labels.get("instance") or labels.get("container") or labels.get("job") or ""
head = name
if severity:
head += f" [{severity}]"
if instance:
head += f" — {instance}"
out = [head]
summary = annotations.get("summary")
description = annotations.get("description")
if summary:
out.append(f" {summary}")
if description and description != summary:
out.append(f" {description}")
return "\n".join(out)
firing = [a for a in alerts if a.get("status") == "firing"] firing = [a for a in alerts if a.get("status") == "firing"]
resolved = [a for a in alerts if a.get("status") == "resolved"] resolved = [a for a in alerts if a.get("status") == "resolved"]
lines = [] # One block per alert, blank-line separated, like the CrowdSec feed
blocks = [format_alert_block(a) for a in firing]
blocks += [format_alert_block(a, resolved=True) for a in resolved]
# Title carries the counts, so the body never has to repeat them
counts = []
if firing: if firing:
lines.append(f"🔥 {len(firing)} alerte(s) active(s)") plural = "s" if len(firing) > 1 else ""
lines.extend(format_alert(a) for a in firing) counts.append(f"{len(firing)} alerte{plural} active{plural}")
if resolved: if resolved:
if lines: counts.append(f"{len(resolved)} résolue{'s' if len(resolved) > 1 else ''}")
lines.append("")
lines.append(f"✅ {len(resolved)} résolue(s)")
lines.extend(format_alert(a) for a in resolved)
status = data.get("status", "firing") title = ("🔥" if firing else "✅") + f" Alertmanager - {', '.join(counts)}"
group_name = data.get("groupLabels", {}).get("alertname", "") body = "\n\n".join(blocks)
title = ("🔥" if status == "firing" else "✅") + " Alertmanager"
if group_name:
title += f" — {group_name}"
body = "\n".join(lines).strip()
# Send to Apprise # Send to Apprise
response = requests.post(APPRISE_URL, json={"title": title, "body": body}, timeout=10) response = requests.post(APPRISE_URL, json={"title": title, "body": body}, timeout=10)