feat(alertmanager): track recovered lifecycle receipts

This commit is contained in:
Your Name
2026-07-22 13:51:15 +08:00
parent fb4f8e3d0c
commit 3a6f8badb7
5 changed files with 1483 additions and 6 deletions

View File

@@ -63,6 +63,9 @@ from src.services.alertmanager_llm_guard import (
ALERTMANAGER_LLM_INFLIGHT_LOCK_TTL_SECONDS,
try_acquire_alertmanager_llm_lock,
)
from src.services.alertmanager_recovery_lifecycle import (
handle_alertmanager_resolved,
)
from src.services.approval_db import get_approval_service
from src.services.auto_approve import get_auto_approve_policy
from src.services.auto_repair_service import AutoRepairService
@@ -2274,6 +2277,75 @@ def _trusted_internal_alertmanager_source(request: Request) -> tuple[bool, str]:
return True, forwarded_chain[0]
def _normalized_alertmanager_resolved_identity(
alert: AlertmanagerAlert,
) -> dict[str, str]:
"""Rebuild the same fingerprint identity used by the firing path."""
labels = dict(alert.labels or {})
annotations = dict(alert.annotations or {})
alertname = str(labels.get("alertname") or "UnknownAlert")
alert_type = get_incident_type(alertname)
if alert_type not in {
"k8s_node_failure",
"k8s_pod_crash",
"db_connection_timeout",
"service_404",
"high_cpu",
"high_memory",
"disk_full",
"ssl_expiry",
"custom",
}:
alert_type = "custom"
severity = {
"critical": "critical",
"warning": "warning",
"info": "info",
}.get(str(labels.get("severity") or "warning").casefold(), "warning")
instance = str(labels.get("instance") or "")
instance_clean = instance.split(":")[0] if ":" in instance else instance
target_resource = str(
labels.get("component")
or labels.get("pod")
or labels.get("name")
or labels.get("container")
or labels.get("job")
or (
instance_clean
if instance_clean and not is_internal_ip(instance_clean)
else ""
)
or alertname
)
namespace = str(labels.get("namespace") or "default")
message = str(
annotations.get("summary")
or annotations.get("description")
or alertname
)
normalized_alert = AlertPayload(
alert_type=alert_type,
severity=severity,
source="alertmanager",
target_resource=target_resource,
namespace=namespace,
message=message,
metrics={},
labels=labels,
)
fingerprint = AlertAnalyzer.generate_fingerprint(normalized_alert)
return {
"alertname": alertname,
"alert_type": alert_type,
"severity": severity,
"target_resource": target_resource,
"namespace": namespace,
"message": message,
"fingerprint": fingerprint,
}
async def _process_new_alert_background(
alert_context: dict,
alert_id: str,
@@ -3350,9 +3422,38 @@ async def alertmanager_webhook(
alert_count=len(payload.alerts),
)
# 只處理第一個 firing 告警 (避免告警風暴)
# 只處理第一個 firing 告警;若本批只有 resolved走同 fingerprint
# canonical lifecycle重複恢復不得重發 Telegram。
firing_alerts = [a for a in payload.alerts if a.status == "firing"]
if not firing_alerts:
resolved_alerts = [a for a in payload.alerts if a.status == "resolved"]
if resolved_alerts:
resolved_alert = resolved_alerts[0]
identity = _normalized_alertmanager_resolved_identity(resolved_alert)
source_alert_id = str(
resolved_alert.fingerprint or identity["fingerprint"]
)
recovery = await handle_alertmanager_resolved(
fingerprint=identity["fingerprint"],
alertname=identity["alertname"],
severity=identity["severity"],
namespace=identity["namespace"],
target_resource=identity["target_resource"],
source_alert_id=source_alert_id,
source_ends_at=str(resolved_alert.endsAt or ""),
)
_record_webhook_metric("success")
return AlertResponse(
success=True,
message=(
"Alertmanager resolved lifecycle: "
f"{recovery.status}; provider_ack="
f"{str(recovery.provider_acknowledged).lower()}"
),
alert_id=source_alert_id,
approval_created=False,
converged=recovery.duplicate,
)
_record_webhook_metric("success")
return AlertResponse(
success=True,

View File

@@ -0,0 +1,767 @@
"""Canonical Alertmanager resolved/recovered lifecycle with send-once Telegram ack.
The lifecycle is keyed by the normalized AWOOOI alert fingerprint. Repeated
resolved webhooks update one durable row; retries are bounded and a known
provider send is never repeated without acknowledgement reconciliation. A
recovery is acknowledged only when Telegram proves the canonical destination
and provider message identity.
"""
from __future__ import annotations
import hashlib
import json
from collections.abc import Awaitable, Callable, Mapping
from dataclasses import dataclass
from datetime import UTC, datetime
from html import escape
from typing import Any
from uuid import NAMESPACE_URL, UUID, uuid5
import structlog
from sqlalchemy import text
from src.services.audit_sink import sanitize
logger = structlog.get_logger(__name__)
_RECOVERY_RESERVATION_TTL_SECONDS = 300
_MAX_RECOVERY_DELIVERY_ATTEMPTS = 3
CorrelationLookup = Callable[[str], Awaitable[dict[str, Any] | None]]
RecoveryReservation = Callable[..., Awaitable[dict[str, Any]]]
IncidentResolver = Callable[[str], Awaitable[bool]]
RecoverySender = Callable[..., Awaitable[dict[str, Any]]]
OutcomeRecorder = Callable[..., Awaitable[None]]
@dataclass(frozen=True)
class AlertmanagerRecoveryResult:
"""Public-safe result for one resolved webhook."""
status: str
fingerprint: str
incident_id: str
lifecycle_event_id: str
duplicate: bool
provider_acknowledged: bool
provider_send_performed: bool
def _lifecycle_provider_event_id(fingerprint: str) -> str:
safe_fingerprint = str(fingerprint or "").strip()[:64] or "missing-fingerprint"
return f"alertmanager:lifecycle:{safe_fingerprint}"
def _parse_timestamp(value: object) -> datetime | None:
try:
parsed = datetime.fromisoformat(str(value or ""))
except ValueError:
return None
if parsed.tzinfo is None:
return parsed.replace(tzinfo=UTC)
return parsed.astimezone(UTC)
def _reservation_decision(
lifecycle: Mapping[str, Any] | None,
*,
incident_id: str,
now: datetime,
) -> dict[str, Any]:
"""Choose one send/retry decision without touching runtime state."""
current = dict(lifecycle or {})
status = str(current.get("status") or "")
attempts = max(0, int(current.get("delivery_attempts") or 0))
current_incident_id = str(current.get("incident_id") or "")
if incident_id and current_incident_id and incident_id != current_incident_id:
return {
"admitted": True,
"status": "recovery_pending_delivery",
"attempts": 1,
"new_incident_cycle": True,
}
if status == "recovered_acknowledged":
return {
"admitted": False,
"status": "duplicate_recovered_suppressed",
"attempts": attempts,
}
if current.get("provider_send_performed") is True:
return {
"admitted": False,
"status": "recovery_delivery_ack_unresolved",
"attempts": attempts,
}
if not incident_id:
return {
"admitted": False,
"status": "resolved_identity_unmatched",
"attempts": attempts,
}
if status == "recovery_pending_delivery":
reserved_at = _parse_timestamp(current.get("reserved_at"))
if (
reserved_at is not None
and (now - reserved_at).total_seconds() < _RECOVERY_RESERVATION_TTL_SECONDS
):
return {
"admitted": False,
"status": "duplicate_recovery_inflight_suppressed",
"attempts": attempts,
}
if attempts >= _MAX_RECOVERY_DELIVERY_ATTEMPTS:
return {
"admitted": False,
"status": "recovery_delivery_retry_exhausted",
"attempts": attempts,
}
return {
"admitted": True,
"status": "recovery_pending_delivery",
"attempts": attempts + 1,
}
def _merge_reservation_observation(
lifecycle: Mapping[str, Any] | None,
*,
decision: Mapping[str, Any],
observed: Mapping[str, Any],
now: datetime,
is_new: bool,
) -> dict[str, Any]:
"""Persist observations without letting duplicates rewrite lifecycle truth."""
updated = dict(lifecycle or {})
if decision.get("new_incident_cycle") is True:
completed_cycles = list(updated.get("completed_cycles") or [])
if updated.get("incident_id"):
completed_cycles.append(
{
"incident_id": updated.get("incident_id"),
"approval_id": updated.get("approval_id"),
"status": updated.get("status"),
"delivery_attempts": updated.get("delivery_attempts"),
"provider_send_performed": updated.get("provider_send_performed"),
"provider_acknowledged": updated.get("provider_acknowledged"),
"delivery_ack": updated.get("delivery_ack") or {},
"completed_at": updated.get("completed_at"),
}
)
updated["completed_cycles"] = completed_cycles[-8:]
updated.update(observed)
updated.update(
{
"last_observed_at": now.isoformat(),
"executor_invocation_allowed": False,
"agent99_dispatch_allowed": False,
"paid_provider_call_allowed": False,
"runtime_infrastructure_mutation_allowed": False,
}
)
if decision["admitted"]:
for stale_key in (
"completed_at",
"incident_resolution_succeeded",
"last_duplicate_at",
"last_duplicate_status",
):
updated.pop(stale_key, None)
updated.update(
{
"status": decision["status"],
"delivery_attempts": decision["attempts"],
"reserved_at": now.isoformat(),
"provider_send_performed": False,
"provider_acknowledged": False,
"delivery_ack": {},
}
)
elif is_new:
updated.update(
{
"status": decision["status"],
"delivery_attempts": decision["attempts"],
"reserved_at": None,
"provider_send_performed": False,
"provider_acknowledged": False,
}
)
else:
updated.update(
{
"last_duplicate_status": decision["status"],
"last_duplicate_at": now.isoformat(),
}
)
return updated
async def lookup_alertmanager_incident_correlation(
fingerprint: str,
) -> dict[str, Any] | None:
"""Read the latest durable approval/incident identity for one fingerprint."""
from src.db.base import get_db_context
async with get_db_context("awoooi") as db:
result = await db.execute(
text(
"""
SELECT approval.id, approval.incident_id,
approval.telegram_message_id
FROM approval_records approval
JOIN incidents incident
ON incident.incident_id = approval.incident_id
WHERE approval.fingerprint = :fingerprint
AND approval.incident_id IS NOT NULL
AND incident.resolved_at IS NULL
AND upper(coalesce(incident.status::text, ''))
NOT IN ('RESOLVED', 'CLOSED')
ORDER BY approval.last_seen_at DESC, approval.created_at DESC
LIMIT 1
"""
),
{"fingerprint": fingerprint},
)
row = result.fetchone()
if row is None:
return None
return {
"approval_id": str(row[0]),
"incident_id": str(row[1]),
"telegram_message_id": int(row[2]) if row[2] is not None else None,
}
async def reserve_alertmanager_recovery(
*,
fingerprint: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
source_alert_id: str,
source_ends_at: str,
correlation: Mapping[str, Any] | None,
) -> dict[str, Any]:
"""Atomically reserve one recovery delivery on the canonical lifecycle row."""
from src.db.base import get_db_context
now = datetime.now(UTC)
provider_event_id = _lifecycle_provider_event_id(fingerprint)
run_id = uuid5(NAMESPACE_URL, f"awooop:alertmanager-lifecycle:{fingerprint}")
correlated_incident_id = str((correlation or {}).get("incident_id") or "")
correlated_approval_id = str((correlation or {}).get("approval_id") or "")
correlated_message_id = (correlation or {}).get("telegram_message_id")
lock_key = f"awoooi:{provider_event_id}"
async with get_db_context("awoooi") as db:
await db.execute(
text("SELECT pg_advisory_xact_lock(hashtextextended(:lock_key, 0))"),
{"lock_key": lock_key},
)
selected = await db.execute(
text(
"""
SELECT event_id, source_envelope
FROM awooop_conversation_event
WHERE project_id = 'awoooi'
AND channel_type = 'internal'
AND provider_event_id = :provider_event_id
FOR UPDATE
"""
),
{"provider_event_id": provider_event_id},
)
existing = selected.fetchone()
envelope: dict[str, Any]
if existing is None:
event_id = uuid5(
NAMESPACE_URL,
f"awooop:alertmanager-lifecycle-event:{fingerprint}",
)
envelope = {
"schema_version": "alertmanager_canonical_lifecycle_v1",
"alertmanager_lifecycle": {},
}
else:
event_id = UUID(str(existing[0]))
raw_envelope = existing[1]
if isinstance(raw_envelope, str):
raw_envelope = json.loads(raw_envelope)
envelope = (
dict(raw_envelope)
if isinstance(raw_envelope, Mapping)
else {
"schema_version": "alertmanager_canonical_lifecycle_v1",
"alertmanager_lifecycle": {},
}
)
current_lifecycle = envelope.get("alertmanager_lifecycle")
current = current_lifecycle if isinstance(current_lifecycle, Mapping) else None
incident_id = correlated_incident_id or str(
(current or {}).get("incident_id") or ""
)
approval_id = correlated_approval_id or str(
(current or {}).get("approval_id") or ""
)
telegram_message_id = (
int(correlated_message_id)
if correlated_message_id is not None
else int((current or {}).get("telegram_message_id"))
if (current or {}).get("telegram_message_id") is not None
else None
)
decision = _reservation_decision(
current,
incident_id=incident_id,
now=now,
)
lifecycle = _merge_reservation_observation(
current,
decision=decision,
observed={
"fingerprint": fingerprint,
"alertname": alertname,
"severity": severity,
"namespace": namespace,
"target_resource": target_resource,
"source_alert_id": source_alert_id,
"source_ends_at": source_ends_at,
"approval_id": approval_id or (current or {}).get("approval_id"),
"incident_id": incident_id or (current or {}).get("incident_id"),
"telegram_message_id": telegram_message_id,
},
now=now,
is_new=existing is None,
)
envelope["alertmanager_lifecycle"] = lifecycle
safe_envelope = sanitize(envelope)
serialized = json.dumps(safe_envelope, ensure_ascii=False, default=str)
raw_preview = (
f"Alertmanager lifecycle {decision['status']}: "
f"{alertname} {incident_id or 'unmatched'}"
)
preview = str(sanitize({"preview": raw_preview})["preview"])[:256]
content_hash = hashlib.sha256(fingerprint.encode("utf-8")).hexdigest()
if existing is None:
await db.execute(
text(
"""
INSERT INTO awooop_conversation_event (
event_id, project_id, channel_type, provider_event_id,
platform_subject_id, channel_user_id, channel_chat_id,
run_id, content_type, content_hash, content_preview,
content_redacted, redaction_version, source_envelope,
is_duplicate, provider_ts, received_at
) VALUES (
:event_id, 'awoooi', 'internal', :provider_event_id,
'alertmanager', 'alertmanager', :channel_chat_id,
:run_id, 'text', :content_hash, :preview,
:preview, 'audit_sink_v1', CAST(:source_envelope AS jsonb),
FALSE, NOW(), NOW()
)
"""
),
{
"event_id": event_id,
"provider_event_id": provider_event_id,
"channel_chat_id": "alertmanager:internal",
"run_id": run_id,
"content_hash": content_hash,
"preview": preview,
"source_envelope": serialized,
},
)
else:
await db.execute(
text(
"""
UPDATE awooop_conversation_event
SET source_envelope = CAST(:source_envelope AS jsonb),
content_preview = :preview,
content_redacted = :preview,
is_duplicate = :is_duplicate,
provider_ts = NOW()
WHERE event_id = :event_id
"""
),
{
"event_id": event_id,
"source_envelope": serialized,
"preview": preview,
"is_duplicate": not decision["admitted"],
},
)
return {
"event_id": str(event_id),
"admitted": bool(decision["admitted"]),
"status": str(decision["status"]),
"duplicate": not bool(decision["admitted"]),
"delivery_attempt": int(decision["attempts"]),
"incident_id": incident_id,
"telegram_message_id": telegram_message_id,
"provider_acknowledged": bool(lifecycle.get("provider_acknowledged")),
"provider_send_performed": bool(lifecycle.get("provider_send_performed")),
}
async def resolve_alertmanager_incident(incident_id: str) -> bool:
"""Resolve the correlated incident without emitting a second postmortem card."""
from src.services.incident_service import get_incident_service
incident = await get_incident_service().resolve_incident(
incident_id,
resolution_type="alertmanager_recovered",
emit_postmortem=False,
)
return incident is not None
def _delivery_ack(result: Mapping[str, Any]) -> dict[str, Any]:
route = result.get("_awoooi_canonical_route_receipt")
context = result.get("_awoooi_delivery_context")
provider_result = result.get("result")
return {
"provider_message_id": (
str(provider_result.get("message_id"))
if isinstance(provider_result, Mapping)
and provider_result.get("message_id") is not None
else None
),
"sender_bot_alias": (
route.get("sender_bot_alias") if isinstance(route, Mapping) else None
),
"destination_alias": (
route.get("destination_alias") if isinstance(route, Mapping) else None
),
"destination_binding": (
route.get("destination_binding") if isinstance(route, Mapping) else None
),
"provider_destination_binding": (
context.get("provider_destination_binding")
if isinstance(context, Mapping)
else None
),
"provider_destination_verification_method": (
context.get("provider_destination_verification_method")
if isinstance(context, Mapping)
else None
),
}
def _outcome_matches_current_cycle(
lifecycle: Mapping[str, Any],
*,
incident_id: str,
delivery_attempt: int,
acknowledged: bool,
) -> bool:
"""Reject stale outcomes while allowing a late verified ack to win."""
if str(lifecycle.get("incident_id") or "") != incident_id:
return False
current_attempt = max(0, int(lifecycle.get("delivery_attempts") or 0))
if current_attempt != delivery_attempt and not acknowledged:
return False
if lifecycle.get("status") == "recovered_acknowledged" and not acknowledged:
return False
return True
async def send_alertmanager_recovery(
*,
incident_id: str,
alertname: str,
severity: str,
target_resource: str,
fingerprint: str,
reply_to_message_id: int | None,
) -> dict[str, Any]:
"""Send one canonical recovery card and return only verified ack metadata."""
from src.services.telegram_gateway import (
_telegram_send_delivery_succeeded,
get_telegram_gateway,
)
route_severity = {
"critical": "P0",
"warning": "P1",
"info": "P2",
}.get(severity.casefold(), "P1")
body = (
"✅ <b>ALERT RECOVERED</b>\n"
"━━━━━━━━━━━━━━━━━━━\n"
f"📋 <code>{escape(incident_id)}</code>\n"
f"🔔 告警:<code>{escape(alertname)}</code>\n"
f"🎯 資產:<code>{escape(target_resource)}</code>\n"
f"🔗 fingerprint<code>{escape(fingerprint[:32])}</code>\n\n"
"Alertmanager 已回報 resolvedincident lifecycle 已更新。"
)
result = await get_telegram_gateway().send_canonical_message(
product_id="awoooi",
signal_family="incident_lifecycle",
severity=route_severity,
text=body,
reply_to_message_id=reply_to_message_id,
)
acknowledged = _telegram_send_delivery_succeeded(result)
return {
"acknowledged": acknowledged,
"provider_send_performed": bool(
isinstance(result, Mapping)
and result.get("_awooop_provider_send_performed") is True
),
"ack": _delivery_ack(result) if isinstance(result, Mapping) else {},
}
async def record_alertmanager_recovery_outcome(
*,
lifecycle_event_id: str,
fingerprint: str,
incident_id: str,
delivery_attempt: int,
incident_resolution_succeeded: bool,
delivery: Mapping[str, Any],
) -> None:
"""Finalize the same lifecycle row with incident and destination ack truth."""
from src.db.base import get_db_context
acknowledged = bool(delivery.get("acknowledged") is True)
provider_send_performed = bool(delivery.get("provider_send_performed") is True)
status = (
"incident_resolution_failed"
if not incident_resolution_succeeded
else "recovered_acknowledged"
if acknowledged
else "recovery_delivery_failed"
)
async with get_db_context("awoooi") as db:
selected = await db.execute(
text(
"""
SELECT source_envelope
FROM awooop_conversation_event
WHERE event_id = CAST(:event_id AS uuid)
AND project_id = 'awoooi'
AND provider_event_id = :provider_event_id
FOR UPDATE
"""
),
{
"event_id": lifecycle_event_id,
"provider_event_id": _lifecycle_provider_event_id(fingerprint),
},
)
row = selected.fetchone()
if row is None:
raise RuntimeError("alertmanager_lifecycle_receipt_missing")
raw_envelope = row[0]
if isinstance(raw_envelope, str):
raw_envelope = json.loads(raw_envelope)
envelope = dict(raw_envelope) if isinstance(raw_envelope, Mapping) else {}
lifecycle = envelope.get("alertmanager_lifecycle")
lifecycle = dict(lifecycle) if isinstance(lifecycle, Mapping) else {}
if not _outcome_matches_current_cycle(
lifecycle,
incident_id=incident_id,
delivery_attempt=delivery_attempt,
acknowledged=acknowledged,
):
logger.info(
"alertmanager_recovery_stale_outcome_suppressed",
fingerprint=fingerprint,
incident_id=incident_id,
delivery_attempt=delivery_attempt,
)
return
lifecycle.update(
{
"status": status,
"incident_resolution_succeeded": incident_resolution_succeeded,
"provider_send_performed": provider_send_performed,
"provider_acknowledged": acknowledged,
"delivery_ack": dict(delivery.get("ack") or {}),
"completed_at": datetime.now(UTC).isoformat(),
}
)
envelope["alertmanager_lifecycle"] = lifecycle
serialized = json.dumps(sanitize(envelope), ensure_ascii=False, default=str)
await db.execute(
text(
"""
UPDATE awooop_conversation_event
SET source_envelope = CAST(:source_envelope AS jsonb),
content_preview = :preview,
content_redacted = :preview,
is_duplicate = FALSE,
provider_ts = NOW()
WHERE event_id = CAST(:event_id AS uuid)
"""
),
{
"event_id": lifecycle_event_id,
"source_envelope": serialized,
"preview": f"Alertmanager lifecycle {status}"[:256],
},
)
async def handle_alertmanager_resolved(
*,
fingerprint: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
source_alert_id: str,
source_ends_at: str,
correlation_lookup: CorrelationLookup = lookup_alertmanager_incident_correlation,
recovery_reservation: RecoveryReservation = reserve_alertmanager_recovery,
incident_resolver: IncidentResolver = resolve_alertmanager_incident,
recovery_sender: RecoverySender = send_alertmanager_recovery,
outcome_recorder: OutcomeRecorder = record_alertmanager_recovery_outcome,
) -> AlertmanagerRecoveryResult:
"""Resolve, notify, and acknowledge one canonical recovery transition."""
try:
correlation = await correlation_lookup(fingerprint)
reservation = await recovery_reservation(
fingerprint=fingerprint,
alertname=alertname,
severity=severity,
namespace=namespace,
target_resource=target_resource,
source_alert_id=source_alert_id,
source_ends_at=source_ends_at,
correlation=correlation,
)
except Exception as exc: # noqa: BLE001 - no durable receipt means no action
logger.warning(
"alertmanager_recovery_reservation_failed",
fingerprint=fingerprint,
error=type(exc).__name__,
)
return AlertmanagerRecoveryResult(
status="durable_lifecycle_unavailable",
fingerprint=fingerprint,
incident_id="",
lifecycle_event_id="",
duplicate=False,
provider_acknowledged=False,
provider_send_performed=False,
)
incident_id = str(
reservation.get("incident_id") or (correlation or {}).get("incident_id") or ""
)
lifecycle_event_id = str(reservation.get("event_id") or "")
delivery_attempt = int(reservation.get("delivery_attempt") or 0)
if reservation.get("admitted") is not True:
return AlertmanagerRecoveryResult(
status=str(reservation.get("status") or "recovery_suppressed"),
fingerprint=fingerprint,
incident_id=incident_id,
lifecycle_event_id=lifecycle_event_id,
duplicate=bool(reservation.get("duplicate") is True),
provider_acknowledged=bool(
reservation.get("provider_acknowledged") is True
),
provider_send_performed=bool(
reservation.get("provider_send_performed") is True
),
)
try:
incident_resolved = await incident_resolver(incident_id)
except Exception as exc: # noqa: BLE001 - persist a retryable terminal
logger.warning(
"alertmanager_incident_resolution_failed",
fingerprint=fingerprint,
incident_id=incident_id,
error=type(exc).__name__,
)
incident_resolved = False
if not incident_resolved:
await outcome_recorder(
lifecycle_event_id=lifecycle_event_id,
fingerprint=fingerprint,
incident_id=incident_id,
delivery_attempt=delivery_attempt,
incident_resolution_succeeded=False,
delivery={
"acknowledged": False,
"provider_send_performed": False,
"ack": {},
},
)
return AlertmanagerRecoveryResult(
status="incident_resolution_failed",
fingerprint=fingerprint,
incident_id=incident_id,
lifecycle_event_id=lifecycle_event_id,
duplicate=False,
provider_acknowledged=False,
provider_send_performed=False,
)
try:
delivery = await recovery_sender(
incident_id=incident_id,
alertname=alertname,
severity=severity,
target_resource=target_resource,
fingerprint=fingerprint,
reply_to_message_id=(
int(reservation["telegram_message_id"])
if reservation.get("telegram_message_id") is not None
else int(correlation["telegram_message_id"])
if correlation and correlation.get("telegram_message_id") is not None
else None
),
)
except Exception as exc: # noqa: BLE001 - durable failure remains retryable
logger.warning(
"alertmanager_recovery_delivery_failed",
fingerprint=fingerprint,
incident_id=incident_id,
error=type(exc).__name__,
)
delivery = {
"acknowledged": False,
"provider_send_performed": False,
"ack": {},
}
await outcome_recorder(
lifecycle_event_id=lifecycle_event_id,
fingerprint=fingerprint,
incident_id=incident_id,
delivery_attempt=delivery_attempt,
incident_resolution_succeeded=True,
delivery=delivery,
)
acknowledged = bool(delivery.get("acknowledged") is True)
return AlertmanagerRecoveryResult(
status=(
"recovered_acknowledged" if acknowledged else "recovery_delivery_failed"
),
fingerprint=fingerprint,
incident_id=incident_id,
lifecycle_event_id=lifecycle_event_id,
duplicate=False,
provider_acknowledged=acknowledged,
provider_send_performed=bool(delivery.get("provider_send_performed") is True),
)