| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "jsonwrap.h" |
| 4 | #include "jsonwrap-internal.h" |
| 5 | |
| 6 | struct dimensions_sorted_walkthrough_data { |
| 7 | BUFFER *wb; |
| 8 | struct summary_total_counts *totals; |
| 9 | QUERY_TARGET *qt; |
| 10 | size_t cardinality_limit; |
| 11 | size_t count; |
| 12 | QUERY_METRICS_COUNTS aggregated_metrics; |
| 13 | STORAGE_POINT aggregated_points; |
| 14 | NETDATA_DOUBLE remaining_contribution; |
| 15 | size_t remaining_count; |
| 16 | }; |
| 17 | |
| 18 | struct dimensions_sorted_entry { |
| 19 | const char *id; |
| 20 | const char *name; |
| 21 | STORAGE_POINT query_points; |
| 22 | QUERY_METRICS_COUNTS metrics; |
| 23 | uint32_t priority; |
| 24 | }; |
| 25 | |
| 26 | static int dimensions_sorted_walktrhough_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { |
| 27 | struct dimensions_sorted_walkthrough_data *sdwd = data; |
| 28 | BUFFER *wb = sdwd->wb; |
| 29 | struct summary_total_counts *totals = sdwd->totals; |
| 30 | QUERY_TARGET *qt = sdwd->qt; |
| 31 | struct dimensions_sorted_entry *z = value; |
| 32 | |
| 33 | sdwd->count++; |
| 34 | |
| 35 | // Check if we need to apply cardinality limiting |
| 36 | if (sdwd->cardinality_limit > 0 && sdwd->count > sdwd->cardinality_limit - 1) { |
| 37 | // This dimension exceeds our limit - aggregate it |
| 38 | |
| 39 | // Increment our remaining count |
| 40 | sdwd->remaining_count++; |
| 41 | |
| 42 | // Calculate contribution for this dimension |
| 43 | if (qt->query_points.sum > 0) { |
| 44 | NETDATA_DOUBLE contribution = z->query_points.sum * 100.0 / qt->query_points.sum; |
| 45 | sdwd->remaining_contribution += contribution; |
| 46 | } |
| 47 | |
| 48 | // Aggregate metrics counts |
| 49 | aggregate_metrics_counts(&sdwd->aggregated_metrics, &z->metrics); |
| 50 | |
| 51 | // Aggregate points |
| 52 | storage_point_merge_to(sdwd->aggregated_points, z->query_points); |
| 53 | |
| 54 | // Still add this to summary totals |
| 55 | aggregate_into_summary_totals(totals, &z->metrics); |
| 56 | |
| 57 | return 1; // Continue processing next dimension |
| 58 | } |
| 59 | |
| 60 | // Output this dimension normally |
| 61 | buffer_json_add_array_item_object(wb); |
| 62 | buffer_json_member_add_string(wb, "id", z->id); |
| 63 | if (z->id != z->name && z->name) |
| 64 | buffer_json_member_add_string(wb, JSKEY(name), z->name); |
| 65 | |
| 66 | // Only include detailed statistics if MINIMAL_STATS option is not set |
| 67 | if (!(qt->window.options & RRDR_OPTION_MINIMAL_STATS)) { |
| 68 | query_target_metric_counts(wb, &z->metrics); |
| 69 | } |
| 70 | |
| 71 | query_target_points_statistics(wb, qt, &z->query_points); |
| 72 | buffer_json_member_add_uint64(wb, JSKEY(priority), z->priority); |
| 73 | buffer_json_object_close(wb); |
| 74 | |
| 75 | aggregate_into_summary_totals(totals, &z->metrics); |
| 76 | |
| 77 | return 1; |
| 78 | } |
| 79 | |
| 80 | // Standard sort by priority |
| 81 | static int dimensions_sorted_priority_compar(const DICTIONARY_ITEM **item1, const DICTIONARY_ITEM **item2) { |
| 82 | struct dimensions_sorted_entry *z1 = dictionary_acquired_item_value(*item1); |
| 83 | struct dimensions_sorted_entry *z2 = dictionary_acquired_item_value(*item2); |
| 84 | |
| 85 | if(z1->priority == z2->priority) |
| 86 | return strcmp(dictionary_acquired_item_name(*item1), dictionary_acquired_item_name(*item2)); |
| 87 | else if(z1->priority < z2->priority) |
| 88 | return -1; |
| 89 | else |
| 90 | return 1; |
| 91 | } |
| 92 | |
| 93 | // Sort by sum value (highest first), then priority |
| 94 | static int dimensions_sorted_sum_compar(const DICTIONARY_ITEM **item1, const DICTIONARY_ITEM **item2) { |
| 95 | struct dimensions_sorted_entry *z1 = dictionary_acquired_item_value(*item1); |
| 96 | struct dimensions_sorted_entry *z2 = dictionary_acquired_item_value(*item2); |
| 97 | |
| 98 | // Sort by sum (highest first) |
| 99 | if (z1->query_points.sum > z2->query_points.sum) return -1; |
| 100 | if (z1->query_points.sum < z2->query_points.sum) return 1; |
| 101 | |
| 102 | // If equal sum, use priority as a secondary sort |
| 103 | if (z1->priority != z2->priority) { |
| 104 | if (z1->priority < z2->priority) return -1; |
| 105 | else return 1; |
| 106 | } |
| 107 | |
| 108 | // If still equal, sort alphabetically |
| 109 | return strcmp(dictionary_acquired_item_name(*item1), dictionary_acquired_item_name(*item2)); |
| 110 | } |
| 111 | |
| 112 | void query_target_summary_dimensions_v12(BUFFER *wb, QUERY_TARGET *qt, const char *key, bool v2, struct summary_total_counts *totals) { |
| 113 | char buf[RRD_ID_LENGTH_MAX * 2 + 2]; |
| 114 | |
| 115 | buffer_json_member_add_array(wb, key); |
| 116 | DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); |
| 117 | struct dimensions_sorted_entry *z; |
| 118 | size_t q = 0; |
| 119 | for (long c = 0; c < (long) qt->dimensions.used; c++) { |
| 120 | QUERY_DIMENSION * qd = query_dimension(qt, c); |
| 121 | RRDMETRIC_ACQUIRED *rma = qd->rma; |
| 122 | |
| 123 | QUERY_METRIC *qm = NULL; |
| 124 | for( ; q < qt->query.used ;q++) { |
| 125 | QUERY_METRIC *tqm = query_metric(qt, q); |
| 126 | QUERY_DIMENSION *tqd = query_dimension(qt, tqm->link.query_dimension_id); |
| 127 | if(tqd->rma != rma) break; |
| 128 | qm = tqm; |
| 129 | } |
| 130 | |
| 131 | const char *k, *id, *name; |
| 132 | |
| 133 | if(v2) { |
| 134 | k = rrdmetric_acquired_name(rma); |
| 135 | if(unlikely(!k || !*k)) |
| 136 | k = rrdmetric_acquired_id(rma); |
| 137 | if(unlikely(!k || !*k)) { |
| 138 | internal_error(true, "QUERY: dimension at index %ld has empty id and name; skipping it", c); |
| 139 | continue; |
| 140 | } |
| 141 | id = k; |
| 142 | name = k; |
| 143 | } |
| 144 | else { |
| 145 | snprintfz(buf, RRD_ID_LENGTH_MAX * 2 + 1, "%s:%s", |
| 146 | rrdmetric_acquired_id(rma), |
| 147 | rrdmetric_acquired_name(rma)); |
| 148 | k = buf; |
| 149 | id = rrdmetric_acquired_id(rma); |
| 150 | name = rrdmetric_acquired_name(rma); |
| 151 | } |
| 152 | |
| 153 | z = dictionary_set(dict, k, NULL, sizeof(*z)); |
| 154 | if(!z->id) { |
| 155 | z->id = id; |
| 156 | z->name = name; |
| 157 | z->priority = qd->priority; |
| 158 | } |
| 159 | else { |
| 160 | if(qd->priority < z->priority) |
| 161 | z->priority = qd->priority; |
| 162 | } |
| 163 | |
| 164 | if(qm) { |
| 165 | z->metrics.selected += (qm->status & RRDR_DIMENSION_SELECTED) ? 1 : 0; |
| 166 | z->metrics.failed += (qm->status & RRDR_DIMENSION_FAILED) ? 1 : 0; |
| 167 | |
| 168 | if(qm->status & RRDR_DIMENSION_QUERIED) { |
| 169 | z->metrics.queried++; |
| 170 | storage_point_merge_to(z->query_points, qm->query_points); |
| 171 | } |
| 172 | } |
| 173 | else |
| 174 | z->metrics.excluded++; |
| 175 | } |
| 176 | |
| 177 | if(v2) { |
| 178 | size_t cardinality_limit = qt->request.cardinality_limit; |
| 179 | size_t dict_entries = dictionary_entries(dict); |
| 180 | |
| 181 | // Use the enhanced walkthrough with cardinality limiting |
| 182 | struct dimensions_sorted_walkthrough_data t = { |
| 183 | .wb = wb, |
| 184 | .totals = totals, |
| 185 | .qt = qt, |
| 186 | .cardinality_limit = cardinality_limit, |
| 187 | .count = 0, |
| 188 | .remaining_count = 0, |
| 189 | .remaining_contribution = 0.0, |
| 190 | .aggregated_metrics = {0}, |
| 191 | .aggregated_points = STORAGE_POINT_UNSET |
| 192 | }; |
| 193 | |
| 194 | // Execute the sorted walkthrough which will either output dimensions directly |
| 195 | // or aggregate them if they exceed the cardinality limit |
| 196 | // Choose the appropriate comparison function based on cardinality limiting |
| 197 | dict_item_comparator_t comparator = (cardinality_limit > 0 && dict_entries > cardinality_limit) ? |
| 198 | dimensions_sorted_sum_compar : |
| 199 | dimensions_sorted_priority_compar; |
| 200 | |
| 201 | if (comparator) |
| 202 | dictionary_sorted_walkthrough_rw(dict, DICTIONARY_LOCK_READ, |
| 203 | dimensions_sorted_walktrhough_cb, &t, comparator); |
| 204 | else |
| 205 | dictionary_walkthrough_rw(dict, DICTIONARY_LOCK_READ, |
| 206 | dimensions_sorted_walktrhough_cb, &t); |
| 207 | |
| 208 | |
| 209 | // Add the aggregated "remaining" dimension if there are any |
| 210 | if (t.remaining_count > 0) { |
| 211 | buffer_json_add_array_item_object(wb); |
| 212 | |
| 213 | // Add basic info for the aggregated dimension |
| 214 | char remaining_label[50]; |
| 215 | snprintfz(remaining_label, sizeof(remaining_label), "remaining %zu dimensions", t.remaining_count); |
| 216 | |
| 217 | buffer_json_member_add_string(wb, "id", "__remaining_dimensions__"); |
| 218 | buffer_json_member_add_string(wb, JSKEY(name), remaining_label); |
| 219 | buffer_json_member_add_double(wb, JSKEY(contribution), t.remaining_contribution); |
| 220 | |
| 221 | // Only include detailed statistics if MINIMAL_STATS option is not set |
| 222 | if (!(qt->window.options & RRDR_OPTION_MINIMAL_STATS)) { |
| 223 | query_target_metric_counts(wb, &t.aggregated_metrics); |
| 224 | } |
| 225 | |
| 226 | query_target_points_statistics(wb, qt, &t.aggregated_points); |
| 227 | buffer_json_object_close(wb); |
| 228 | } |
| 229 | } |
| 230 | else { |
| 231 | // v1 |
| 232 | dfe_start_read(dict, z) { |
| 233 | buffer_json_add_array_item_array(wb); |
| 234 | buffer_json_add_array_item_string(wb, z->id); |
| 235 | buffer_json_add_array_item_string(wb, z->name); |
| 236 | buffer_json_array_close(wb); |
| 237 | } |
| 238 | dfe_done(z); |
| 239 | } |
| 240 | dictionary_destroy(dict); |
| 241 | buffer_json_array_close(wb); |
| 242 | } |