Fix missing resolved notifications for recovered heartbeats
Summary
During testing of the Heartbeats functionality in IncidentRelay, a notification gap was identified:
- heartbeat overdue alerts were created correctly;
- the initial notification was sent;
- escalation and matcher-based channel selection worked;
- the alert was resolved when the heartbeat recovered and Auto Resolve was enabled;
- however, no resolved notification was sent to the configured notification channels.
The standard alert lifecycle already sends resolved notifications through the notification-policy pipeline. The heartbeat recovery flow resolved the alert but did not call that pipeline.
Target use case
- A heartbeat ping is received by IncidentRelay.
- The heartbeat stops sending pings.
- After the configured interval and grace period:
- the heartbeat becomes overdue;
- an alert is created;
- the initial notification is sent;
- escalation continues according to the escalation policy;
- matcher-based notification rules select the required Teams channel.
- The heartbeat starts sending pings again.
- When Auto Resolve is enabled:
- the heartbeat returns to
ok;
- the related alert is resolved;
- a resolved notification is sent to the matching notification channels.
- When Auto Resolve is disabled:
- the heartbeat returns to
ok;
- the related alert remains open;
- no resolved notification is sent.
Example heartbeat labels:
{
"Application": "DB",
"source_type": "heartbeat"
}
Changed files
One application file was modified:
app/services/heartbeats/service.py
In addition, the existing notification-policy rule configuration was updated to include the resolved event type.
Change 1: send resolved notifications after heartbeat recovery
Modified file
app/services/heartbeats/service.py
Affected functions
_resolve_current_overdue_alert()
_resolve_current_instance_overdue_alert()
Previous behavior
The heartbeat recovery flow called:
resolved = resolve_alert(group.id, user_id=None)
This correctly:
- changed the alert group status to
resolved;
- created the standard
resolved timeline event;
- refreshed service and business-impact state.
After that, the heartbeat-specific recovery event was created:
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_recovered",
message=f"Heartbeat recovered: {heartbeat.name}",
)
However, the resolved notification pipeline was not called.
As a result, the alert was resolved in IncidentRelay, but Microsoft Teams did not receive a recovery/resolved message.
Standard lifecycle behavior
The standard incoming-alert lifecycle in:
app/services/alerts/lifecycle.py
sends resolved notifications using:
if group.last_notification_at:
notify_alert(
group,
event_type="resolved",
)
The heartbeat recovery flow did not use the same logic.
Updated behavior
The heartbeat recovery functions now call the existing notification delivery pipeline after resolving the alert:
resolved = resolve_alert(group.id, user_id=None)
if resolved.last_notification_at:
notify_alert(
resolved,
event_type="resolved",
)
The heartbeat-specific recovery event is then recorded as before.
Required import
Modified file
app/services/heartbeats/service.py
The following import was added:
from app.services.integrations.auth import create_raw_token, hash_token
+from app.services.notifications.delivery import notify_alert
Change 2: resolved notification for a regular heartbeat
Modified function
_resolve_current_overdue_alert()
Previous code
resolved = resolve_alert(group.id, user_id=None)
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_recovered",
message=f"Heartbeat recovered: {heartbeat.name}",
)
return resolved
Updated code
resolved = resolve_alert(group.id, user_id=None)
if resolved.last_notification_at:
notify_alert(
resolved,
event_type="resolved",
)
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_recovered",
message=f"Heartbeat recovered: {heartbeat.name}",
)
return resolved
Conceptual diff — app/services/heartbeats/service.py
resolved = resolve_alert(group.id, user_id=None)
+if resolved.last_notification_at:
+ notify_alert(
+ resolved,
+ event_type="resolved",
+ )
+
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_recovered",
message=f"Heartbeat recovered: {heartbeat.name}",
)
Change 3: resolved notification for a heartbeat instance
Modified function
_resolve_current_instance_overdue_alert()
Previous code
resolved = resolve_alert(group.id, user_id=None)
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_instance_recovered",
message=(
f"Heartbeat recovered: "
f"{instance.heartbeat.name} / {instance.instance_key}"
),
)
return resolved
Updated code
resolved = resolve_alert(group.id, user_id=None)
if resolved.last_notification_at:
notify_alert(
resolved,
event_type="resolved",
)
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_instance_recovered",
message=(
f"Heartbeat recovered: "
f"{instance.heartbeat.name} / {instance.instance_key}"
),
)
return resolved
Conceptual diff — app/services/heartbeats/service.py
resolved = resolve_alert(group.id, user_id=None)
+if resolved.last_notification_at:
+ notify_alert(
+ resolved,
+ event_type="resolved",
+ )
+
alerts_repo.create_alert_event(
group_id=resolved.id,
event_type="heartbeat_instance_recovered",
message=(
f"Heartbeat recovered: "
f"{instance.heartbeat.name} / {instance.instance_key}"
),
)
Why last_notification_at is checked
The standard IncidentRelay lifecycle sends a resolved notification only when the alert group had already produced an outbound notification.
The same guard was retained:
if resolved.last_notification_at:
This avoids sending a resolved message for an alert that never had an initial notification.
It also keeps the heartbeat recovery behavior consistent with:
app/services/alerts/lifecycle.py
Notification Policy configuration
The notification-policy rules used by the heartbeat service also need to allow the resolved event type.
Example event-type configuration:
[
"notification",
"reminder",
"resolved"
]
For the Database Layer rule:
[
"escalation",
"resolved"
]
The exact event-type assignment depends on which channels should receive recovery notifications.
No direct Teams webhook call was added. Resolved notifications continue to pass through:
Heartbeat recovery
→ resolve_alert()
→ notify_alert(event_type="resolved")
→ Notification Policy resolver
→ matcher evaluation
→ configured notification channel
Expected result
When Auto Resolve is enabled:
- the heartbeat returns to
ok;
- the related alert is resolved;
- the standard
resolved event is created;
- the matching notification-policy rule is selected;
- the configured Teams channel receives a resolved notification;
- the heartbeat-specific
heartbeat_recovered event is recorded.
When Auto Resolve is disabled:
- the heartbeat returns to
ok;
- the existing alert remains open;
- no resolved notification is sent.
Validation
Test configuration
Heartbeat interval: 60 seconds
Grace period: 30 seconds
Severity: critical
Priority: p1
Application label: DB
Notification channel: Microsoft Teams
Notification mode: service notification policy
Auto Resolve enabled
The end-to-end test completed successfully:
Heartbeat stops sending pings
→ heartbeat becomes overdue
→ alert is created
→ initial Duty Engineer notification
→ escalation after configured delay
→ Database Layer rotation
→ Database on-call engineer assignment
→ dedicated Database Teams notification
→ heartbeat ping is received
→ heartbeat status changes to ok
→ alert is resolved
→ resolved Teams notification is sent
Expected timeline events were observed:
heartbeat_overdue
notification_sent
escalated
notification_sent
resolved
heartbeat_recovered
The notification delivery also created the corresponding Teams delivery event.
Auto Resolve disabled
The second scenario also completed successfully:
Heartbeat stops sending pings
→ heartbeat becomes overdue
→ alert is created
→ notifications and escalation work
→ heartbeat ping is received
→ heartbeat status changes to ok
→ alert remains open
→ no resolved notification is sent
This behavior matches the Auto Resolve setting.
Request for review
Please review and confirm:
-
Heartbeat recovery should use the same resolved-notification pipeline as the standard incoming-alert lifecycle.
-
The update in:
app/services/heartbeats/service.py
is consistent with the intended heartbeat lifecycle.
-
Checking last_notification_at is the correct condition before sending a resolved notification.
-
Both regular heartbeats and heartbeat instances should use the same behavior.
-
Notification-policy rules should explicitly include the resolved event type for channels that must receive recovery messages.
-
Automated tests should cover:
- heartbeat recovery with Auto Resolve enabled;
- heartbeat recovery with Auto Resolve disabled;
- recovery after the initial notification;
- recovery before any notification was sent;
- matcher-based resolved channel selection;
- heartbeat-instance recovery;
- prevention of duplicate resolved notifications.
Deployment details
The solution is deployed on-premises on Oracle Linux 9.
Architecture
Heartbeat client
│
│ HTTP ping using a heartbeat token
▼
Nginx
│
▼
Gunicorn :8081
│
▼
IncidentRelay Flask application
│
├── PostgreSQL
├── Microsoft Teams via Power Automate webhook
└── IncidentRelay scheduler worker
Components
Application version directory:
/var/www/incidentrelay-1.2.1
Active application path:
/var/www/incidentrelay
Modified application file:
/var/www/incidentrelay/app/services/heartbeats/service.py
Python virtual environment:
/var/www/incidentrelay/venv
Python executable:
/var/www/incidentrelay/venv/bin/python
Configuration file:
/etc/incidentrelay/incidentrelay.conf
Web application service:
incidentrelay.service
Scheduler service:
incidentrelay-scheduler.service
Database:
PostgreSQL
Database name:
incidentrelay
Application server:
Gunicorn
Application port:
8081
Reverse proxy:
Nginx
Local deployment procedure
A timestamped backup of the modified Python file was created before applying the change.
The notification-policy rule configuration was also backed up before adding the resolved event type.
Syntax validation:
/var/www/incidentrelay/venv/bin/python -m py_compile \
/var/www/incidentrelay/app/services/heartbeats/service.py
Import validation:
cd /var/www/incidentrelay
sudo -u incidentrelay env \
PYTHONPATH=/var/www/incidentrelay \
INCIDENTRELAY_CONFIG_FILE=/etc/incidentrelay/incidentrelay.conf \
/var/www/incidentrelay/venv/bin/python -c \
'from app.services.heartbeats import service'
Service restart:
systemctl restart incidentrelay.service
systemctl restart incidentrelay-scheduler.service
Service verification:
systemctl status incidentrelay.service
systemctl status incidentrelay-scheduler.service
The final behavior was validated with new heartbeat overdue and recovery cycles.
Fix missing resolved notifications for recovered heartbeats
Summary
During testing of the Heartbeats functionality in IncidentRelay, a notification gap was identified:
The standard alert lifecycle already sends resolved notifications through the notification-policy pipeline. The heartbeat recovery flow resolved the alert but did not call that pipeline.
Target use case
ok;ok;Example heartbeat labels:
{ "Application": "DB", "source_type": "heartbeat" }Changed files
One application file was modified:
In addition, the existing notification-policy rule configuration was updated to include the
resolvedevent type.Change 1: send resolved notifications after heartbeat recovery
Modified file
Affected functions
Previous behavior
The heartbeat recovery flow called:
This correctly:
resolved;resolvedtimeline event;After that, the heartbeat-specific recovery event was created:
However, the resolved notification pipeline was not called.
As a result, the alert was resolved in IncidentRelay, but Microsoft Teams did not receive a recovery/resolved message.
Standard lifecycle behavior
The standard incoming-alert lifecycle in:
sends resolved notifications using:
The heartbeat recovery flow did not use the same logic.
Updated behavior
The heartbeat recovery functions now call the existing notification delivery pipeline after resolving the alert:
The heartbeat-specific recovery event is then recorded as before.
Required import
Modified file
The following import was added:
from app.services.integrations.auth import create_raw_token, hash_token +from app.services.notifications.delivery import notify_alertChange 2: resolved notification for a regular heartbeat
Modified function
_resolve_current_overdue_alert()Previous code
Updated code
Conceptual diff —
app/services/heartbeats/service.pyChange 3: resolved notification for a heartbeat instance
Modified function
_resolve_current_instance_overdue_alert()Previous code
Updated code
Conceptual diff —
app/services/heartbeats/service.pyWhy
last_notification_atis checkedThe standard IncidentRelay lifecycle sends a resolved notification only when the alert group had already produced an outbound notification.
The same guard was retained:
This avoids sending a resolved message for an alert that never had an initial notification.
It also keeps the heartbeat recovery behavior consistent with:
Notification Policy configuration
The notification-policy rules used by the heartbeat service also need to allow the
resolvedevent type.Example event-type configuration:
For the Database Layer rule:
The exact event-type assignment depends on which channels should receive recovery notifications.
No direct Teams webhook call was added. Resolved notifications continue to pass through:
Expected result
When Auto Resolve is enabled:
ok;resolvedevent is created;heartbeat_recoveredevent is recorded.When Auto Resolve is disabled:
ok;Validation
Test configuration
Auto Resolve enabled
The end-to-end test completed successfully:
Expected timeline events were observed:
The notification delivery also created the corresponding Teams delivery event.
Auto Resolve disabled
The second scenario also completed successfully:
This behavior matches the Auto Resolve setting.
Request for review
Please review and confirm:
Heartbeat recovery should use the same resolved-notification pipeline as the standard incoming-alert lifecycle.
The update in:
is consistent with the intended heartbeat lifecycle.
Checking
last_notification_atis the correct condition before sending a resolved notification.Both regular heartbeats and heartbeat instances should use the same behavior.
Notification-policy rules should explicitly include the
resolvedevent type for channels that must receive recovery messages.Automated tests should cover:
Deployment details
The solution is deployed on-premises on Oracle Linux 9.
Architecture
Components
Local deployment procedure
A timestamped backup of the modified Python file was created before applying the change.
The notification-policy rule configuration was also backed up before adding the
resolvedevent type.Syntax validation:
Import validation:
Service restart:
Service verification:
The final behavior was validated with new heartbeat overdue and recovery cycles.