@cryptotaxi247 / netdata-1 / commits / 64e0d811d

Speed up alerts API filtering with host status snapshots (#21984)

* Refactor health alert handling and snapshot management: - Introduced `health_alert_status_counts` for better organization of alert status data. - Implemented host-level alert status snapshot for efficient prefiltering. - Optimized alert summary handling with conditional dictionary initialization based on the SUMMARY flag. - Added atomic operations for consistent alert status tracking and updates. - Enhanced health event loop to dynamically adjust alert status counts. * Fix seqlock window: defer begin_update to right before publish

Stelios Fragkakis committed Mar 20, 2026 at 14:05 UTC 64e0d811d6d622ad01bbfa2bbed8ff8f4295b97f
4 files changed +270 -87
src/database/contexts/api_v2_contexts.c
+63 -1
@@ -563,6 +563,56 @@ static void rrdcontext_to_json_v2_rrdhost(BUFFER *wb, RRDHOST *host, struct rrdc
563 buffer_json_object_close(wb); // this node
564 }
565
566 +static bool rrdhost_alert_status_snapshot_read(RRDHOST *host, struct health_alert_status_counts *snapshot) {
567 + for(size_t retries = 0; retries < 3; retries++) {
568 + uint64_t g1 = __atomic_load_n(&host->health.alert_status_snapshot.generation, __ATOMIC_ACQUIRE);
569 + if(unlikely(g1 & 1))
570 + continue;
571 +
572 + snapshot->clear = __atomic_load_n(&host->health.alert_status_snapshot.counts.clear, __ATOMIC_RELAXED);
573 + snapshot->warning = __atomic_load_n(&host->health.alert_status_snapshot.counts.warning, __ATOMIC_RELAXED);
574 + snapshot->critical = __atomic_load_n(&host->health.alert_status_snapshot.counts.critical, __ATOMIC_RELAXED);
575 + snapshot->undefined = __atomic_load_n(&host->health.alert_status_snapshot.counts.undefined, __ATOMIC_RELAXED);
576 + snapshot->uninitialized = __atomic_load_n(&host->health.alert_status_snapshot.counts.uninitialized, __ATOMIC_RELAXED);
577 +
578 + uint8_t valid = __atomic_load_n(&host->health.alert_status_snapshot.valid, __ATOMIC_ACQUIRE);
579 + uint64_t g2 = __atomic_load_n(&host->health.alert_status_snapshot.generation, __ATOMIC_ACQUIRE);
580 + if(likely(g1 == g2 && !(g2 & 1)))
581 + return valid;
582 + }
583 +
584 + return false;
585 +}
586 +
587 +static inline bool rrdhost_alert_status_snapshot_matches_filter(
588 + CONTEXTS_ALERT_STATUS filter,
589 + const struct health_alert_status_counts *snapshot) {
590 +
591 + if(!(filter & CONTEXTS_ALERT_STATUSES))
592 + return true;
593 +
594 + if((filter & CONTEXT_ALERT_UNINITIALIZED) && snapshot->uninitialized)
595 + return true;
596 +
597 + if((filter & CONTEXT_ALERT_UNDEFINED) && snapshot->undefined)
598 + return true;
599 +
600 + if((filter & CONTEXT_ALERT_CLEAR) && snapshot->clear)
601 + return true;
602 +
603 + if((filter & CONTEXT_ALERT_WARNING) && snapshot->warning)
604 + return true;
605 +
606 + if((filter & CONTEXT_ALERT_CRITICAL) && snapshot->critical)
607 + return true;
608 +
609 + if((filter & CONTEXT_ALERT_RAISED) &&
610 + (snapshot->warning || snapshot->critical))
611 + return true;
612 +
613 + return false;
614 +}
615 +
616 static ssize_t rrdcontext_to_json_v2_add_host(void *data, RRDHOST *host, bool queryable_host) {
617 if(!queryable_host || !host->rrdctx.contexts)
618 // the host matches the 'scope_host' but does not match the 'host' patterns
@@ -583,8 +633,20 @@ static ssize_t rrdcontext_to_json_v2_add_host(void *data, RRDHOST *host, bool qu
633 // interrupted
634 return -1; // stop the query
635
586 - bool host_matched = (ctl->mode & (CONTEXTS_V2_NODES | CONTEXTS_V2_FUNCTIONS | CONTEXTS_V2_ALERTS)) && !ctl->contexts.pattern && !ctl->contexts.scope_pattern && !ctl->window.enabled;
636 + bool host_may_have_matching_alerts = true;
637 + if((ctl->mode & CONTEXTS_V2_ALERTS) && (ctl->request->alerts.status & CONTEXTS_ALERT_STATUSES)) {
638 + struct health_alert_status_counts snapshot = { 0 };
639 + if(rrdhost_alert_status_snapshot_read(host, &snapshot) &&
640 + !rrdhost_alert_status_snapshot_matches_filter(ctl->request->alerts.status, &snapshot))
641 + host_may_have_matching_alerts = false;
642 + }
643 +
644 + CONTEXTS_V2_MODE host_matched_modes = CONTEXTS_V2_NODES | CONTEXTS_V2_FUNCTIONS |
645 + (host_may_have_matching_alerts ? CONTEXTS_V2_ALERTS : 0);
646 + bool host_matched = (ctl->mode & host_matched_modes) && !ctl->contexts.pattern && !ctl->contexts.scope_pattern && !ctl->window.enabled;
647 bool do_contexts = (ctl->mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS)) || ctl->contexts.pattern || ctl->contexts.scope_pattern;
648 + if((ctl->mode & CONTEXTS_V2_ALERTS) && !host_may_have_matching_alerts)
649 + do_contexts = false;
650
651 if(do_contexts) {
652 ssize_t added = query_scope_foreach_context(
src/database/contexts/api_v2_contexts_alerts.c
+93 -84
@@ -43,6 +43,7 @@ struct alert_by_x_entry {
43 };
44
45 bool rrdcontext_matches_alert(struct rrdcontext_to_json_v2_data *ctl, RRDCONTEXT *rc) {
46 + const bool summary_requested = (ctl->request->options & CONTEXTS_OPTION_SUMMARY);
47 size_t matches = 0;
48 RRDINSTANCE *ri;
49 dfe_start_read(rc->rrdinstances, ri) {
@@ -87,54 +88,58 @@ bool rrdcontext_matches_alert(struct rrdcontext_to_json_v2_data *ctl, RRDCONTEXT
88 continue;
89 }
90
90 - struct alert_v2_entry t = {
91 - .tmp = rcl,
92 - };
93 - struct alert_v2_entry *a2e =
94 - dictionary_set(ctl->alerts.summary, string2str(rcl->config.name),
95 - &t, sizeof(struct alert_v2_entry));
96 - size_t ati = a2e->ati;
97 - matches++;
91 + size_t ati = 0;
92 + if(summary_requested) {
93 + struct alert_v2_entry t = {
94 + .tmp = rcl,
95 + };
96 + struct alert_v2_entry *a2e =
97 + dictionary_set(ctl->alerts.summary, string2str(rcl->config.name),
98 + &t, sizeof(struct alert_v2_entry));
99 + ati = a2e->ati;
100 +
101 + dictionary_set_advanced(ctl->alerts.by_type,
102 + string2str(rcl->config.type),
103 + (ssize_t)string_strlen(rcl->config.type),
104 + NULL,
105 + sizeof(struct alert_by_x_entry),
106 + rcl);
107 +
108 + dictionary_set_advanced(ctl->alerts.by_component,
109 + string2str(rcl->config.component),
110 + (ssize_t)string_strlen(rcl->config.component),
111 + NULL,
112 + sizeof(struct alert_by_x_entry),
113 + rcl);
114 +
115 + dictionary_set_advanced(ctl->alerts.by_classification,
116 + string2str(rcl->config.classification),
117 + (ssize_t)string_strlen(rcl->config.classification),
118 + NULL,
119 + sizeof(struct alert_by_x_entry),
120 + rcl);
121 +
122 + dictionary_set_advanced(ctl->alerts.by_recipient,
123 + string2str(rcl->config.recipient),
124 + (ssize_t)string_strlen(rcl->config.recipient),
125 + NULL,
126 + sizeof(struct alert_by_x_entry),
127 + rcl);
128 +
129 + char module[128];
130 + rrdlabels_get_value_strcpyz(st->rrdlabels, module, sizeof(module), "_collect_module");
131 + if(!*module)
132 + strncpyz(module, "[unset]", sizeof(module) - 1);
133 +
134 + dictionary_set_advanced(ctl->alerts.by_module,
135 + module,
136 + -1,
137 + NULL,
138 + sizeof(struct alert_by_x_entry),
139 + rcl);
140 + }
141
99 - dictionary_set_advanced(ctl->alerts.by_type,
100 - string2str(rcl->config.type),
101 - (ssize_t)string_strlen(rcl->config.type),
102 - NULL,
103 - sizeof(struct alert_by_x_entry),
104 - rcl);
105 -
106 - dictionary_set_advanced(ctl->alerts.by_component,
107 - string2str(rcl->config.component),
108 - (ssize_t)string_strlen(rcl->config.component),
109 - NULL,
110 - sizeof(struct alert_by_x_entry),
111 - rcl);
112 -
113 - dictionary_set_advanced(ctl->alerts.by_classification,
114 - string2str(rcl->config.classification),
115 - (ssize_t)string_strlen(rcl->config.classification),
116 - NULL,
117 - sizeof(struct alert_by_x_entry),
118 - rcl);
119 -
120 - dictionary_set_advanced(ctl->alerts.by_recipient,
121 - string2str(rcl->config.recipient),
122 - (ssize_t)string_strlen(rcl->config.recipient),
123 - NULL,
124 - sizeof(struct alert_by_x_entry),
125 - rcl);
126 -
127 - char module[128];
128 - rrdlabels_get_value_strcpyz(st->rrdlabels, module, sizeof(module), "_collect_module");
129 - if(!*module)
130 - strncpyz(module, "[unset]", sizeof(module) - 1);
131 -
132 - dictionary_set_advanced(ctl->alerts.by_module,
133 - module,
134 - -1,
135 - NULL,
136 - sizeof(struct alert_by_x_entry),
137 - rcl);
142 + matches++;
143
144 if (ctl->options & (CONTEXTS_OPTION_INSTANCES | CONTEXTS_OPTION_VALUES)) {
145 char key[20 + 1];
@@ -737,60 +742,64 @@ static void rrdcontext_v2_set_transition_filter(const char *machine_guid, const
742 }
743
744 bool rrdcontexts_v2_init_alert_dictionaries(struct rrdcontext_to_json_v2_data *ctl, struct api_v2_contexts_request *req) {
745 + const bool summary_requested = (req->options & CONTEXTS_OPTION_SUMMARY);
746 +
747 if(req->alerts.transition) {
748 ctl->options |= CONTEXTS_OPTION_INSTANCES | CONTEXTS_OPTION_VALUES;
749 if(!sql_find_alert_transition(req->alerts.transition, rrdcontext_v2_set_transition_filter, ctl))
750 return false;
751 }
752
746 - ctl->alerts.summary = dictionary_create_advanced(
747 - DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
748 - NULL,
749 - sizeof(struct alert_v2_entry));
753 + if(summary_requested) {
754 + ctl->alerts.summary = dictionary_create_advanced(
755 + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
756 + NULL,
757 + sizeof(struct alert_v2_entry));
758
751 - dictionary_register_insert_callback(ctl->alerts.summary, alerts_v2_insert_callback, ctl);
752 - dictionary_register_conflict_callback(ctl->alerts.summary, alerts_v2_conflict_callback, ctl);
753 - dictionary_register_delete_callback(ctl->alerts.summary, alerts_v2_delete_callback, ctl);
759 + dictionary_register_insert_callback(ctl->alerts.summary, alerts_v2_insert_callback, ctl);
760 + dictionary_register_conflict_callback(ctl->alerts.summary, alerts_v2_conflict_callback, ctl);
761 + dictionary_register_delete_callback(ctl->alerts.summary, alerts_v2_delete_callback, ctl);
762
755 - ctl->alerts.by_type = dictionary_create_advanced(
756 - DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
757 - NULL,
758 - sizeof(struct alert_by_x_entry));
763 + ctl->alerts.by_type = dictionary_create_advanced(
764 + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
765 + NULL,
766 + sizeof(struct alert_by_x_entry));
767
760 - dictionary_register_insert_callback(ctl->alerts.by_type, alerts_by_x_insert_callback, NULL);
761 - dictionary_register_conflict_callback(ctl->alerts.by_type, alerts_by_x_conflict_callback, NULL);
768 + dictionary_register_insert_callback(ctl->alerts.by_type, alerts_by_x_insert_callback, NULL);
769 + dictionary_register_conflict_callback(ctl->alerts.by_type, alerts_by_x_conflict_callback, NULL);
770
763 - ctl->alerts.by_component = dictionary_create_advanced(
764 - DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
765 - NULL,
766 - sizeof(struct alert_by_x_entry));
771 + ctl->alerts.by_component = dictionary_create_advanced(
772 + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
773 + NULL,
774 + sizeof(struct alert_by_x_entry));
775
768 - dictionary_register_insert_callback(ctl->alerts.by_component, alerts_by_x_insert_callback, NULL);
769 - dictionary_register_conflict_callback(ctl->alerts.by_component, alerts_by_x_conflict_callback, NULL);
776 + dictionary_register_insert_callback(ctl->alerts.by_component, alerts_by_x_insert_callback, NULL);
777 + dictionary_register_conflict_callback(ctl->alerts.by_component, alerts_by_x_conflict_callback, NULL);
778
771 - ctl->alerts.by_classification = dictionary_create_advanced(
772 - DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
773 - NULL,
774 - sizeof(struct alert_by_x_entry));
779 + ctl->alerts.by_classification = dictionary_create_advanced(
780 + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
781 + NULL,
782 + sizeof(struct alert_by_x_entry));
783
776 - dictionary_register_insert_callback(ctl->alerts.by_classification, alerts_by_x_insert_callback, NULL);
777 - dictionary_register_conflict_callback(ctl->alerts.by_classification, alerts_by_x_conflict_callback, NULL);
784 + dictionary_register_insert_callback(ctl->alerts.by_classification, alerts_by_x_insert_callback, NULL);
785 + dictionary_register_conflict_callback(ctl->alerts.by_classification, alerts_by_x_conflict_callback, NULL);
786
779 - ctl->alerts.by_recipient = dictionary_create_advanced(
780 - DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
781 - NULL,
782 - sizeof(struct alert_by_x_entry));
787 + ctl->alerts.by_recipient = dictionary_create_advanced(
788 + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
789 + NULL,
790 + sizeof(struct alert_by_x_entry));
791
784 - dictionary_register_insert_callback(ctl->alerts.by_recipient, alerts_by_x_insert_callback, NULL);
785 - dictionary_register_conflict_callback(ctl->alerts.by_recipient, alerts_by_x_conflict_callback, NULL);
792 + dictionary_register_insert_callback(ctl->alerts.by_recipient, alerts_by_x_insert_callback, NULL);
793 + dictionary_register_conflict_callback(ctl->alerts.by_recipient, alerts_by_x_conflict_callback, NULL);
794
787 - ctl->alerts.by_module = dictionary_create_advanced(
788 - DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
789 - NULL,
790 - sizeof(struct alert_by_x_entry));
795 + ctl->alerts.by_module = dictionary_create_advanced(
796 + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
797 + NULL,
798 + sizeof(struct alert_by_x_entry));
799
792 - dictionary_register_insert_callback(ctl->alerts.by_module, alerts_by_x_insert_callback, NULL);
793 - dictionary_register_conflict_callback(ctl->alerts.by_module, alerts_by_x_conflict_callback, NULL);
800 + dictionary_register_insert_callback(ctl->alerts.by_module, alerts_by_x_insert_callback, NULL);
801 + dictionary_register_conflict_callback(ctl->alerts.by_module, alerts_by_x_conflict_callback, NULL);
802 + }
803
804 if(ctl->options & (CONTEXTS_OPTION_INSTANCES | CONTEXTS_OPTION_VALUES)) {
805 ctl->alerts.alert_instances = dictionary_create_advanced(
src/health/health-alert-log.h
+15
@@ -14,7 +14,22 @@ typedef struct alarm_log {
14 RW_SPINLOCK spinlock;
15 } ALARM_LOG;
16
17 +struct health_alert_status_counts {
18 + uint32_t clear;
19 + uint32_t warning;
20 + uint32_t critical;
21 + uint32_t undefined;
22 + uint32_t uninitialized;
23 +};
24 +
25 typedef struct health {
26 + // Recomputed on each host health evaluation and read by alerts APIs as a host-level prefilter.
27 + struct {
28 + uint64_t generation; // seqlock-style generation for consistent readers
29 + struct health_alert_status_counts counts;
30 + uint8_t valid; // 1 when snapshot is fully published
31 + } alert_status_snapshot;
32 +
33 time_t delay_up_to; // a timestamp to delay alarms processing up to
34 STRING *default_exec; // the full path of the alarms notifications program
35 STRING *default_recipient; // the default recipient for all alarms
src/health/health_event_loop.c
+99 -2
@@ -36,6 +36,85 @@ void rrdhost_set_health_evloop_iteration(RRDHOST *host) {
36 health_evloop_current_iteration(), __ATOMIC_RELAXED);
37 }
38
39 +static inline void health_alert_status_counts_add(struct health_alert_status_counts *c, RRDCALC_STATUS status) {
40 + switch(status) {
41 + case RRDCALC_STATUS_CLEAR:
42 + c->clear++;
43 + break;
44 +
45 + case RRDCALC_STATUS_WARNING:
46 + c->warning++;
47 + break;
48 +
49 + case RRDCALC_STATUS_CRITICAL:
50 + c->critical++;
51 + break;
52 +
53 + case RRDCALC_STATUS_UNDEFINED:
54 + c->undefined++;
55 + break;
56 +
57 + case RRDCALC_STATUS_UNINITIALIZED:
58 + c->uninitialized++;
59 + break;
60 +
61 + default:
62 + break;
63 + }
64 +}
65 +
66 +static inline void health_alert_status_counts_sub(struct health_alert_status_counts *c, RRDCALC_STATUS status) {
67 + switch(status) {
68 + case RRDCALC_STATUS_CLEAR:
69 + if(c->clear) c->clear--;
70 + break;
71 +
72 + case RRDCALC_STATUS_WARNING:
73 + if(c->warning) c->warning--;
74 + break;
75 +
76 + case RRDCALC_STATUS_CRITICAL:
77 + if(c->critical) c->critical--;
78 + break;
79 +
80 + case RRDCALC_STATUS_UNDEFINED:
81 + if(c->undefined) c->undefined--;
82 + break;
83 +
84 + case RRDCALC_STATUS_UNINITIALIZED:
85 + if(c->uninitialized) c->uninitialized--;
86 + break;
87 +
88 + default:
89 + break;
90 + }
91 +}
92 +
93 +static inline uint64_t health_alert_status_snapshot_begin_update(RRDHOST *host) {
94 + // Make generation odd (writer in progress) so readers can safely retry/fallback.
95 + uint64_t generation = __atomic_add_fetch(&host->health.alert_status_snapshot.generation, 1, __ATOMIC_ACQ_REL);
96 + if(!(generation & 1))
97 + generation = __atomic_add_fetch(&host->health.alert_status_snapshot.generation, 1, __ATOMIC_ACQ_REL);
98 +
99 + __atomic_store_n(&host->health.alert_status_snapshot.valid, 0, __ATOMIC_RELEASE);
100 + return generation;
101 +}
102 +
103 +static inline void health_alert_status_snapshot_finish_update(
104 + RRDHOST *host,
105 + const struct health_alert_status_counts *counts,
106 + uint64_t odd_generation) {
107 +
108 + __atomic_store_n(&host->health.alert_status_snapshot.counts.clear, counts->clear, __ATOMIC_RELAXED);
109 + __atomic_store_n(&host->health.alert_status_snapshot.counts.warning, counts->warning, __ATOMIC_RELAXED);
110 + __atomic_store_n(&host->health.alert_status_snapshot.counts.critical, counts->critical, __ATOMIC_RELAXED);
111 + __atomic_store_n(&host->health.alert_status_snapshot.counts.undefined, counts->undefined, __ATOMIC_RELAXED);
112 + __atomic_store_n(&host->health.alert_status_snapshot.counts.uninitialized, counts->uninitialized, __ATOMIC_RELAXED);
113 +
114 + __atomic_store_n(&host->health.alert_status_snapshot.valid, 1, __ATOMIC_RELEASE);
115 + __atomic_store_n(&host->health.alert_status_snapshot.generation, odd_generation + 1, __ATOMIC_RELEASE);
116 +}
117 +
118 // ----------------------------------------------------------------------------
119 // health main thread and friends
120
@@ -240,6 +319,8 @@ static void do_eval_expression(
319 // returns the number of runnable alerts
320 static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_delay, time_t now, time_t *next_run) {
321 size_t runnable = 0;
322 + struct health_alert_status_counts status_counts = { 0 };
323 + bool snapshot_complete = true;
324
325 if(unlikely(!rrdhost_should_run_health(host)))
326 return;
@@ -291,8 +372,13 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
372 // the first loop is to lookup values from the db
373 RRDCALC *rc;
374 foreach_rrdcalc_in_rrdhost_read(host, rc) {
294 - if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host)))
375 + if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host))) {
376 + snapshot_complete = false;
377 break;
378 + }
379 +
380 + if(likely(rc->rrdset))
381 + health_alert_status_counts_add(&status_counts, rc->status);
382
383 rrdcalc_update_info_using_rrdset_labels(rc);
384
@@ -325,6 +411,7 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
411 if (ae) {
412 health_log_alert(host, ae);
413 health_alarm_log_add_entry(host, ae, false);
414 + health_alert_status_counts_sub(&status_counts, rc->status);
415 rc->old_status = rc->status;
416 rc->status = RRDCALC_STATUS_REMOVED;
417 rc->last_status_change = now_tmp;
@@ -424,8 +511,10 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
511
512 if (unlikely(runnable && service_running(SERVICE_HEALTH))) {
513 foreach_rrdcalc_in_rrdhost_read(host, rc) {
427 - if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host)))
514 + if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host))) {
515 + snapshot_complete = false;
516 break;
517 + }
518
519 if (unlikely(!(rc->run_flags & RRDCALC_FLAG_RUNNABLE)))
520 continue;
@@ -534,6 +623,9 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
623 rrdhost_hostname(host), ae_chart_id(ae), ae_name(ae), ae_new_value_string(ae),
624 rrdcalc_status2string(ae->new_status));
625
626 + health_alert_status_counts_sub(&status_counts, rc->status);
627 + health_alert_status_counts_add(&status_counts, status);
628 +
629 rc->last_status_change_value = rc->value;
630 rc->last_status_change = now;
631 rc->old_status = rc->status;
@@ -617,6 +709,11 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
709 foreach_rrdcalc_in_rrdhost_done(rc);
710 }
711
712 + if(likely(snapshot_complete)) {
713 + uint64_t snapshot_generation = health_alert_status_snapshot_begin_update(host);
714 + health_alert_status_snapshot_finish_update(host, &status_counts, snapshot_generation);
715 + }
716 +
717 if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host))) {
718 alerts_raised_summary_free(hrm);
719 return;