-
Notifications
You must be signed in to change notification settings - Fork 20
Add cronjob for syncing heartbeats with heartbeat-missing incidents #1954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
4c6df07
106932e
e99c801
b422c15
06b6b2e
ae446d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Added management command suitable to run as a cronjob that looks for dead | ||
| incidents (as per heartbeat freqeuncy and last seen heartbeat). Dead sources | ||
| are reported as incidents, formerly dead sources get their respective | ||
| incidents auto-closed. | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,112 @@ | ||||||
| import logging | ||||||
| from datetime import datetime, timedelta | ||||||
| from typing import Optional | ||||||
|
|
||||||
| from django.utils import timezone | ||||||
|
|
||||||
| from argus.incident.factories import create_stateful_incident | ||||||
| from argus.incident.models import HEARTBEAT_TAG, SOURCE_TAG_KEY, Incident, SourceSystem | ||||||
|
|
||||||
|
|
||||||
| LOG = logging.getLogger(__name__) | ||||||
| DEFAULT_FUDGE_FACTOR = 10 | ||||||
| DEFAULT_LEVEL = 4 | ||||||
| _FUDGE = timedelta(seconds=DEFAULT_FUDGE_FACTOR) | ||||||
|
hmpf marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| __all__ = [ | ||||||
| "sync_heartbeats_with_heartbeat_incidents", | ||||||
| ] | ||||||
|
|
||||||
|
|
||||||
| def sync_heartbeats_with_heartbeat_incidents(): | ||||||
| timestamp = timezone.now() | ||||||
| alive_sources = _close_incidents_whose_sources_are_alive_again(timestamp) | ||||||
| new_incidents = _create_incidents_for_dead_sources(timestamp) | ||||||
| return alive_sources, new_incidents | ||||||
|
|
||||||
|
|
||||||
| def _close_incidents_whose_sources_are_alive_again(timestamp): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to add a docstring that explains the output of this function |
||||||
| argus = SourceSystem.objects.get(name="argus") | ||||||
| still_dead_sources = SourceSystem.objects.dead(timestamp - _FUDGE) | ||||||
| outdated_incidents = Incident.objects.heartbeat_incidents().open() | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would call this rather
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call what? Which of the four lines?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| format_str = 'Source "{}" is again alive and well' | ||||||
| sources = [] | ||||||
| for incident in outdated_incidents: | ||||||
| source_ids = incident.get_values_for_tag_key(SOURCE_TAG_KEY) | ||||||
| # We need exactly one source-tag. | ||||||
| # We don't need to check for multiple *identical* tags, | ||||||
| # that's a UniqueError, but we should also only have one source-tag | ||||||
| if not source_ids: | ||||||
| LOG.error('Heartbeat incident "%s" is missing source-tag!', incident) | ||||||
|
hmpf marked this conversation as resolved.
Outdated
|
||||||
| continue | ||||||
| elif len(source_ids) > 1: | ||||||
| LOG.error('Heartbeat incident "%s" has multiple source tags!?', incident) | ||||||
| continue | ||||||
|
|
||||||
| source_id = source_ids[0] | ||||||
| if still_dead_sources.filter(pk=source_id).exists(): | ||||||
| continue | ||||||
|
|
||||||
| try: | ||||||
| source = SourceSystem.objects.get(pk=source_id) | ||||||
| except SourceSystem.DoesNotExist: | ||||||
| LOG.error('Heartbeat source "%s" has disappeared!', source_id) | ||||||
|
hmpf marked this conversation as resolved.
Outdated
|
||||||
| continue | ||||||
|
|
||||||
| # phew, *now* we can close the incident | ||||||
| incident.set_end(argus.user, timestamp, format_str.format(source)) | ||||||
| sources.append(source) | ||||||
| return sources | ||||||
|
|
||||||
|
|
||||||
| def _create_incidents_for_dead_sources(timestamp: Optional[datetime] = None): | ||||||
| # Create existing incidents whose sources have become dead | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment confuses me - are you creating them or are they existing? And this should be a docstring |
||||||
| dead_sources = SourceSystem.objects.dead(timestamp - _FUDGE) | ||||||
| incident_owner = SourceSystem.objects.get(name="argus") | ||||||
| # prevent multiple incidents | ||||||
| heartbeat_incidents = Incident.objects.heartbeat_incidents().open() | ||||||
| source_ids = (incident.source_id for incident in heartbeat_incidents) | ||||||
| new_incidents = [] | ||||||
| for source in dead_sources.exclude(id__in=source_ids): | ||||||
| incident = _get_or_create_incident_for_dead_source(source, incident_owner=incident_owner, timestamp=timestamp) | ||||||
| if incident: | ||||||
| new_incidents.append(incident) | ||||||
| return new_incidents | ||||||
|
hmpf marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
|
|
||||||
| def _get_or_create_incident_for_dead_source( | ||||||
| source: SourceSystem, | ||||||
| incident_owner: SourceSystem, | ||||||
| timestamp: Optional[datetime] = None, | ||||||
| level: int = DEFAULT_LEVEL, | ||||||
| ): | ||||||
| assert isinstance(source, SourceSystem), "source is not a SourceSystem" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| if not timestamp: | ||||||
| timestamp = timezone.now() | ||||||
|
|
||||||
| source_tag = f"{SOURCE_TAG_KEY}={source.pk}" | ||||||
| tags = [HEARTBEAT_TAG, source_tag] | ||||||
|
|
||||||
| # One last check in case a heartbeat arrived in the meantime | ||||||
| source.refresh_from_db() | ||||||
| dead = source.is_dead(timestamp - _FUDGE) | ||||||
|
hmpf marked this conversation as resolved.
Outdated
|
||||||
| if dead: | ||||||
| # prevent duplicates | ||||||
| existing_incidents = Incident.objects.heartbeat_incidents().open().from_tags(source_tag) | ||||||
| if existing_incidents.exists(): | ||||||
| try: | ||||||
| return existing_incidents.get() | ||||||
| except Incident.MultipleObjectsReturned: | ||||||
| # TODO: fix, different function? | ||||||
|
Check warning on line 100 in src/argus/incident/heartbeat_utils.py
|
||||||
|
johannaengland marked this conversation as resolved.
Outdated
|
||||||
| LOG.error('Source "%s" has multiple open heartbeat incidents', source) | ||||||
| return existing_incidents.first() | ||||||
| # save new incident | ||||||
| incident = create_stateful_incident( | ||||||
| f"Missing heartbeat from source {source}, dead?", | ||||||
| incident_owner, | ||||||
| level, | ||||||
| tags=tags, | ||||||
| start_time=timestamp, | ||||||
| ) | ||||||
| return incident | ||||||
| return None | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from django.core.management.base import BaseCommand | ||
|
|
||
| from argus.incident.heartbeat_utils import sync_heartbeats_with_heartbeat_incidents | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = "Check that heartbeat-supporting sources are alive" | ||
|
|
||
| def handle(self, *args, **options): | ||
| alive_sources, new_incidents = sync_heartbeats_with_heartbeat_incidents() | ||
| if not (alive_sources or new_incidents): | ||
| return | ||
| if options["verbosity"] > 0: | ||
| for source in alive_sources: | ||
| self.stdout.write(f'Closed incident for source "{source.name}", it\'s back') | ||
| for incident in new_incidents: | ||
| self.stdout.write(f"Created incident {incident}") | ||
| else: | ||
| count_incidents = len(new_incidents) | ||
| self.stdout.write(f"Created {count_incidents} new incidents") | ||
| count_alive = len(alive_sources) | ||
| self.stdout.write(f"Closed {count_alive} existing incidents") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.