| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "api_v2_contexts.h" |
| 4 | #include "../rrdlabels-aggregated.h" |
| 5 | #include "aclk/aclk_capas.h" |
| 6 | #include "web/mcp/mcp.h" |
| 7 | #include "libnetdata/json/json-keys.h" |
| 8 | |
| 9 | // ---------------------------------------------------------------------------- |
| 10 | // /api/v2/contexts API |
| 11 | |
| 12 | // Enum for all match types - using bitmask to track multiple matches |
| 13 | typedef enum { |
| 14 | SEARCH_MATCH_NONE = 0, |
| 15 | SEARCH_MATCH_CONTEXT_ID = (1 << 0), |
| 16 | SEARCH_MATCH_CONTEXT_TITLE = (1 << 1), |
| 17 | SEARCH_MATCH_CONTEXT_UNITS = (1 << 2), |
| 18 | SEARCH_MATCH_CONTEXT_FAMILY = (1 << 3), |
| 19 | SEARCH_MATCH_INSTANCE = (1 << 4), |
| 20 | SEARCH_MATCH_DIMENSION = (1 << 5), |
| 21 | SEARCH_MATCH_LABEL = (1 << 6), |
| 22 | } SEARCH_MATCH_TYPE; |
| 23 | |
| 24 | struct function_v2_entry { |
| 25 | size_t size; |
| 26 | size_t used; |
| 27 | size_t *node_ids; |
| 28 | STRING *help; |
| 29 | STRING *tags; |
| 30 | HTTP_ACCESS access; |
| 31 | int priority; |
| 32 | uint32_t version; |
| 33 | }; |
| 34 | |
| 35 | struct context_v2_entry { |
| 36 | size_t count; |
| 37 | STRING *id; // DO NOT FREE THIS, IT IS NOT DUP'd |
| 38 | STRING *title; |
| 39 | STRING *family; |
| 40 | STRING *units; |
| 41 | uint32_t priority; |
| 42 | time_t first_time_s; |
| 43 | time_t last_time_s; |
| 44 | size_t nodes; |
| 45 | size_t instances; |
| 46 | RRD_FLAGS flags; |
| 47 | DICTIONARY *instances_dict; |
| 48 | DICTIONARY *dimensions_dict; |
| 49 | RRDLABELS_AGGREGATED *labels_aggregated; |
| 50 | |
| 51 | RRDCONTEXT *rc; // THIS IS TEMPORARY, WHILE REFERENCED, NOT TO BE CLEANED |
| 52 | |
| 53 | // For search results |
| 54 | SEARCH_MATCH_TYPE matched_types; // Bitmask of all match types |
| 55 | DICTIONARY *matched_instances; |
| 56 | DICTIONARY *matched_dimensions; |
| 57 | RRDLABELS_AGGREGATED *matched_labels; |
| 58 | }; |
| 59 | |
| 60 | struct category_entry { |
| 61 | size_t count; |
| 62 | DICTIONARY *contexts; // Dictionary to hold context names |
| 63 | }; |
| 64 | |
| 65 | static void rrdcontext_categorize_and_output(BUFFER *wb, DICTIONARY *contexts_dict, size_t cardinality_limit) { |
| 66 | size_t total_contexts = dictionary_entries(contexts_dict); |
| 67 | |
| 68 | // First pass: count categories |
| 69 | DICTIONARY *categories = dictionary_create(DICT_OPTION_SINGLE_THREADED); |
| 70 | |
| 71 | struct context_v2_entry *z; |
| 72 | dfe_start_read(contexts_dict, z) { |
| 73 | const char *context_name = string2str(z->id); |
| 74 | char category[256]; |
| 75 | const char *first_dot = strchr(context_name, '.'); |
| 76 | if (first_dot) { |
| 77 | const char *second_dot = strchr(first_dot + 1, '.'); |
| 78 | if (second_dot) { |
| 79 | // Use up to second dot as category |
| 80 | size_t prefix_len = second_dot - context_name; |
| 81 | if (prefix_len > sizeof(category) - 1) prefix_len = sizeof(category) - 1; |
| 82 | memcpy(category, context_name, prefix_len); |
| 83 | category[prefix_len] = '\0'; |
| 84 | } else { |
| 85 | // Only one dot, use up to first dot |
| 86 | size_t prefix_len = first_dot - context_name; |
| 87 | if (prefix_len > sizeof(category) - 1) prefix_len = sizeof(category) - 1; |
| 88 | memcpy(category, context_name, prefix_len); |
| 89 | category[prefix_len] = '\0'; |
| 90 | } |
| 91 | } else { |
| 92 | strncpyz(category, context_name, sizeof(category) - 1); |
| 93 | } |
| 94 | |
| 95 | struct category_entry *entry = dictionary_get(categories, category); |
| 96 | if (!entry) { |
| 97 | struct category_entry new_entry = {0}; |
| 98 | new_entry.contexts = dictionary_create(DICT_OPTION_SINGLE_THREADED); |
| 99 | entry = dictionary_set(categories, category, &new_entry, sizeof(struct category_entry)); |
| 100 | } |
| 101 | entry->count++; |
| 102 | // Store the context name for sampling |
| 103 | dictionary_set(entry->contexts, context_name, NULL, 0); |
| 104 | } |
| 105 | dfe_done(z); |
| 106 | |
| 107 | // Calculate how many samples per category |
| 108 | size_t num_categories = dictionary_entries(categories); |
| 109 | size_t samples_per_category = 3; // Default to 3 |
| 110 | if (num_categories > 0 && cardinality_limit > 0) { |
| 111 | samples_per_category = cardinality_limit / num_categories; |
| 112 | if (samples_per_category < 3) samples_per_category = 3; |
| 113 | } |
| 114 | |
| 115 | // Add info object first |
| 116 | buffer_json_member_add_object(wb, "__info__"); |
| 117 | buffer_json_member_add_string(wb, "status", "categorized"); |
| 118 | buffer_json_member_add_uint64(wb, "total_contexts", total_contexts); |
| 119 | buffer_json_member_add_uint64(wb, "categories", num_categories); |
| 120 | buffer_json_member_add_uint64(wb, "samples_per_category", samples_per_category); |
| 121 | buffer_json_member_add_string(wb, "help", "Results grouped by category with samples. Use 'metrics' parameter with specific patterns like 'system.*' to get full details for a category."); |
| 122 | buffer_json_object_close(wb); |
| 123 | |
| 124 | // Output categorized contexts |
| 125 | struct category_entry *cat_entry; |
| 126 | dfe_start_read(categories, cat_entry) { |
| 127 | buffer_json_member_add_array(wb, cat_entry_dfe.name); |
| 128 | |
| 129 | size_t samples_shown = 0; |
| 130 | void *ctx_name; |
| 131 | dfe_start_read(cat_entry->contexts, ctx_name) { |
| 132 | // Only show samples_per_category - 1 if we need to add "... more" |
| 133 | size_t max_to_show = (cat_entry->count > samples_per_category) ? samples_per_category - 1 : cat_entry->count; |
| 134 | if (samples_shown < max_to_show) { |
| 135 | buffer_json_add_array_item_string(wb, ctx_name_dfe.name); |
| 136 | samples_shown++; |
| 137 | } else { |
| 138 | break; |
| 139 | } |
| 140 | } |
| 141 | dfe_done(ctx_name); |
| 142 | |
| 143 | // Add "... and X more" only if we have more contexts than the limit |
| 144 | if (cat_entry->count > samples_per_category) { |
| 145 | char msg[100]; |
| 146 | snprintf(msg, sizeof(msg), "... and %zu more", cat_entry->count - samples_shown); |
| 147 | buffer_json_add_array_item_string(wb, msg); |
| 148 | } |
| 149 | |
| 150 | buffer_json_array_close(wb); |
| 151 | } |
| 152 | dfe_done(cat_entry); |
| 153 | |
| 154 | // Cleanup |
| 155 | dfe_start_write(categories, cat_entry) { |
| 156 | dictionary_destroy(cat_entry->contexts); |
| 157 | } |
| 158 | dfe_done(cat_entry); |
| 159 | dictionary_destroy(categories); |
| 160 | } |
| 161 | |
| 162 | static inline bool full_text_search_string(FTS_INDEX *fts, SIMPLE_PATTERN *q, STRING *ptr) { |
| 163 | fts->searches++; |
| 164 | fts->string_searches++; |
| 165 | return simple_pattern_matches_string(q, ptr); |
| 166 | } |
| 167 | |
| 168 | static inline bool full_text_search_char(FTS_INDEX *fts, SIMPLE_PATTERN *q, char *ptr) { |
| 169 | fts->searches++; |
| 170 | fts->char_searches++; |
| 171 | return simple_pattern_matches(q, ptr); |
| 172 | } |
| 173 | |
| 174 | // Structure to hold search results |
| 175 | struct fts_search_results { |
| 176 | SEARCH_MATCH_TYPE matched_types; // Bitmask of all match types |
| 177 | DICTIONARY *matched_instances; |
| 178 | DICTIONARY *matched_dimensions; |
| 179 | RRDLABELS_AGGREGATED *matched_labels; |
| 180 | }; |
| 181 | |
| 182 | static void rrdcontext_to_json_v2_full_text_search(struct rrdcontext_to_json_v2_data *ctl, RRDCONTEXT *rc, SIMPLE_PATTERN *q, struct fts_search_results *results) { |
| 183 | // Initialize results |
| 184 | results->matched_types = SEARCH_MATCH_NONE; |
| 185 | results->matched_instances = NULL; |
| 186 | results->matched_dimensions = NULL; |
| 187 | results->matched_labels = NULL; |
| 188 | |
| 189 | // Check context-level matches |
| 190 | // Always search ID - it's the primary identifier |
| 191 | if(unlikely(full_text_search_string(&ctl->q.fts, q, rc->id))) { |
| 192 | results->matched_types |= SEARCH_MATCH_CONTEXT_ID; |
| 193 | } |
| 194 | |
| 195 | // Only search family if the option is enabled |
| 196 | if((ctl->options & CONTEXTS_OPTION_FAMILY) && unlikely(full_text_search_string(&ctl->q.fts, q, rc->family))) { |
| 197 | results->matched_types |= SEARCH_MATCH_CONTEXT_FAMILY; |
| 198 | } |
| 199 | |
| 200 | // Only search title if the option is enabled |
| 201 | if((ctl->options & CONTEXTS_OPTION_TITLES) && unlikely(full_text_search_string(&ctl->q.fts, q, rc->title))) { |
| 202 | results->matched_types |= SEARCH_MATCH_CONTEXT_TITLE; |
| 203 | } |
| 204 | |
| 205 | // Only search units if the option is enabled |
| 206 | if((ctl->options & CONTEXTS_OPTION_UNITS) && unlikely(full_text_search_string(&ctl->q.fts, q, rc->units))) { |
| 207 | results->matched_types |= SEARCH_MATCH_CONTEXT_UNITS; |
| 208 | } |
| 209 | |
| 210 | |
| 211 | RRDINSTANCE *ri; |
| 212 | dfe_start_read(rc->rrdinstances, ri) { |
| 213 | if(ctl->window.enabled && !query_matches_retention(ctl->window.after, ctl->window.before, ri->first_time_s, (ri->flags & RRD_FLAG_COLLECTED) ? ctl->now : ri->last_time_s, 0)) |
| 214 | continue; |
| 215 | |
| 216 | // Check instance name match only if instances option is enabled |
| 217 | if((ctl->options & CONTEXTS_OPTION_INSTANCES) && |
| 218 | (unlikely(full_text_search_string(&ctl->q.fts, q, ri->id)) || |
| 219 | (ri->name != ri->id && full_text_search_string(&ctl->q.fts, q, ri->name)))) { |
| 220 | if(!results->matched_instances) |
| 221 | results->matched_instances = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, 0); |
| 222 | dictionary_set(results->matched_instances, string2str(ri->name), NULL, 0); |
| 223 | results->matched_types |= SEARCH_MATCH_INSTANCE; |
| 224 | } |
| 225 | |
| 226 | // Check dimensions only if dimensions option is enabled |
| 227 | if(ctl->options & CONTEXTS_OPTION_DIMENSIONS) { |
| 228 | RRDMETRIC *rm; |
| 229 | dfe_start_read(ri->rrdmetrics, rm) { |
| 230 | if(ctl->window.enabled && !query_matches_retention(ctl->window.after, ctl->window.before, rm->first_time_s, (rm->flags & RRD_FLAG_COLLECTED) ? ctl->now : rm->last_time_s, 0)) |
| 231 | continue; |
| 232 | |
| 233 | if(unlikely(full_text_search_string(&ctl->q.fts, q, rm->id)) || |
| 234 | (rm->name != rm->id && full_text_search_string(&ctl->q.fts, q, rm->name))) { |
| 235 | if(!results->matched_dimensions) |
| 236 | results->matched_dimensions = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, 0); |
| 237 | dictionary_set(results->matched_dimensions, string2str(rm->name), NULL, 0); |
| 238 | results->matched_types |= SEARCH_MATCH_DIMENSION; |
| 239 | } |
| 240 | } |
| 241 | dfe_done(rm); |
| 242 | } |
| 243 | |
| 244 | // Check labels only if labels option is enabled |
| 245 | if(ctl->options & CONTEXTS_OPTION_LABELS) { |
| 246 | size_t label_searches = 0; |
| 247 | RRDLABELS *labels = rrdinstance_labels(ri); |
| 248 | if(unlikely(rrdlabels_entries(labels))) { |
| 249 | results->matched_labels = rrdlabels_full_text_search(labels, q, results->matched_labels, &label_searches); |
| 250 | |
| 251 | if(results->matched_labels) { |
| 252 | results->matched_types |= SEARCH_MATCH_LABEL; |
| 253 | } |
| 254 | |
| 255 | ctl->q.fts.searches += label_searches; |
| 256 | ctl->q.fts.char_searches += label_searches; |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | // We don't check alerts anymore as they are less relevant for the search |
| 261 | } |
| 262 | dfe_done(ri); |
| 263 | } |
| 264 | |
| 265 | static ssize_t rrdcontext_to_json_v2_add_context(void *data, RRDCONTEXT_ACQUIRED *rca, bool queryable_context __maybe_unused) { |
| 266 | struct rrdcontext_to_json_v2_data *ctl = data; |
| 267 | |
| 268 | RRDCONTEXT *rc = rrdcontext_acquired_value(rca); |
| 269 | |
| 270 | if(ctl->window.enabled && !query_matches_retention(ctl->window.after, ctl->window.before, rc->first_time_s, (rc->flags & RRD_FLAG_COLLECTED) ? ctl->now : rc->last_time_s, 0)) |
| 271 | return 0; // continue to next context |
| 272 | |
| 273 | struct fts_search_results search_results = {0}; |
| 274 | |
| 275 | if((ctl->mode & CONTEXTS_V2_SEARCH) && ctl->q.pattern) { |
| 276 | rrdcontext_to_json_v2_full_text_search(ctl, rc, ctl->q.pattern, &search_results); |
| 277 | |
| 278 | if(search_results.matched_types == SEARCH_MATCH_NONE) |
| 279 | return 0; // continue to next context |
| 280 | } |
| 281 | |
| 282 | if(ctl->mode & CONTEXTS_V2_ALERTS) { |
| 283 | if(!rrdcontext_matches_alert(ctl, rc)) |
| 284 | return 0; // continue to next context |
| 285 | } |
| 286 | |
| 287 | if(ctl->contexts.dict) { |
| 288 | struct context_v2_entry t = { |
| 289 | .count = 1, |
| 290 | .id = rc->id, |
| 291 | .title = string_dup(rc->title), |
| 292 | .family = string_dup(rc->family), |
| 293 | .units = string_dup(rc->units), |
| 294 | .priority = rc->priority, |
| 295 | .first_time_s = rc->first_time_s, |
| 296 | .last_time_s = rc->last_time_s, |
| 297 | .flags = rc->flags, |
| 298 | .nodes = 1, |
| 299 | .instances = dictionary_entries(rc->rrdinstances), |
| 300 | .instances_dict = NULL, |
| 301 | .dimensions_dict = NULL, |
| 302 | .rc = rc, |
| 303 | // Store search results |
| 304 | .matched_types = search_results.matched_types, |
| 305 | .matched_instances = search_results.matched_instances, |
| 306 | .matched_dimensions = search_results.matched_dimensions, |
| 307 | .matched_labels = search_results.matched_labels, |
| 308 | }; |
| 309 | |
| 310 | dictionary_set(ctl->contexts.dict, string2str(rc->id), &t, sizeof(struct context_v2_entry)); |
| 311 | } |
| 312 | |
| 313 | return 1; |
| 314 | } |
| 315 | |
| 316 | void buffer_json_node_add_v2_mcp(BUFFER *wb, RRDHOST *host, size_t ni __maybe_unused) { |
| 317 | buffer_json_member_add_string(wb, "machine_guid", host->machine_guid); |
| 318 | |
| 319 | if(!UUIDiszero(host->node_id)) |
| 320 | buffer_json_member_add_uuid(wb, "node_id", host->node_id.uuid); |
| 321 | |
| 322 | buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(host)); |
| 323 | |
| 324 | buffer_json_member_add_string(wb, "relationship", |
| 325 | host == localhost ? "localhost" : |
| 326 | (rrdhost_is_virtual(host) ? "virtual" : "child")); |
| 327 | |
| 328 | buffer_json_member_add_boolean(wb, "connected", rrdhost_is_online(host)); |
| 329 | } |
| 330 | |
| 331 | static void rrdhost_receiver_to_json(BUFFER *wb, RRDHOST_STATUS *s, const char *key, CONTEXTS_OPTIONS options) { |
| 332 | buffer_json_member_add_object(wb, key); |
| 333 | { |
| 334 | buffer_json_member_add_uint64(wb, "id", s->ingest.id); |
| 335 | buffer_json_member_add_int64(wb, "hops", s->ingest.hops); |
| 336 | buffer_json_member_add_string(wb, "type", rrdhost_ingest_type_to_string(s->ingest.type)); |
| 337 | buffer_json_member_add_string(wb, "status", rrdhost_ingest_status_to_string(s->ingest.status)); |
| 338 | buffer_json_member_add_time_t_formatted(wb, "since", s->ingest.since, options & CONTEXTS_OPTION_RFC3339); |
| 339 | buffer_json_member_add_time_t(wb, "age", s->now - s->ingest.since); |
| 340 | buffer_json_member_add_uint64(wb, "metrics", s->ingest.collected.metrics); |
| 341 | buffer_json_member_add_uint64(wb, "instances", s->ingest.collected.instances); |
| 342 | buffer_json_member_add_uint64(wb, "contexts", s->ingest.collected.contexts); |
| 343 | |
| 344 | if(s->ingest.type == RRDHOST_INGEST_TYPE_CHILD) { |
| 345 | if(s->ingest.status == RRDHOST_INGEST_STATUS_OFFLINE) |
| 346 | buffer_json_member_add_string(wb, "reason", stream_handshake_error_to_string(s->ingest.reason)); |
| 347 | |
| 348 | if(s->ingest.status == RRDHOST_INGEST_STATUS_REPLICATING) { |
| 349 | buffer_json_member_add_object(wb, "replication"); |
| 350 | { |
| 351 | buffer_json_member_add_boolean(wb, "in_progress", s->ingest.replication.in_progress); |
| 352 | buffer_json_member_add_double(wb, "completion", s->ingest.replication.completion); |
| 353 | buffer_json_member_add_uint64(wb, "instances", s->ingest.replication.instances); |
| 354 | } |
| 355 | buffer_json_object_close(wb); // replication |
| 356 | } |
| 357 | |
| 358 | if(s->ingest.status == RRDHOST_INGEST_STATUS_REPLICATING || s->ingest.status == RRDHOST_INGEST_STATUS_ONLINE) { |
| 359 | buffer_json_member_add_object(wb, "source"); |
| 360 | { |
| 361 | char buf[1024 + 1]; |
| 362 | snprintfz(buf, sizeof(buf) - 1, "[%s]:%d%s", s->ingest.peers.local.ip, s->ingest.peers.local.port, s->ingest.ssl ? ":SSL" : ""); |
| 363 | buffer_json_member_add_string(wb, "local", buf); |
| 364 | |
| 365 | snprintfz(buf, sizeof(buf) - 1, "[%s]:%d%s", s->ingest.peers.peer.ip, s->ingest.peers.peer.port, s->ingest.ssl ? ":SSL" : ""); |
| 366 | buffer_json_member_add_string(wb, "remote", buf); |
| 367 | |
| 368 | stream_capabilities_to_json_array(wb, s->ingest.capabilities, "capabilities"); |
| 369 | } |
| 370 | buffer_json_object_close(wb); // source |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | buffer_json_object_close(wb); // collection |
| 375 | } |
| 376 | |
| 377 | static void rrdhost_sender_to_json(BUFFER *wb, RRDHOST_STATUS *s, const char *key, CONTEXTS_OPTIONS options) { |
| 378 | if(s->stream.status == RRDHOST_STREAM_STATUS_DISABLED) |
| 379 | return; |
| 380 | |
| 381 | buffer_json_member_add_object(wb, key); |
| 382 | { |
| 383 | buffer_json_member_add_uint64(wb, "id", s->stream.id); |
| 384 | buffer_json_member_add_uint64(wb, "hops", s->stream.hops); |
| 385 | buffer_json_member_add_string(wb, "status", rrdhost_streaming_status_to_string(s->stream.status)); |
| 386 | buffer_json_member_add_time_t_formatted(wb, "since", s->stream.since, options & CONTEXTS_OPTION_RFC3339); |
| 387 | buffer_json_member_add_time_t(wb, "age", s->now - s->stream.since); |
| 388 | |
| 389 | if (s->stream.status == RRDHOST_STREAM_STATUS_OFFLINE) |
| 390 | buffer_json_member_add_string(wb, "reason", stream_handshake_error_to_string(s->stream.reason)); |
| 391 | |
| 392 | buffer_json_member_add_object(wb, "replication"); |
| 393 | { |
| 394 | buffer_json_member_add_boolean(wb, "in_progress", s->stream.replication.in_progress); |
| 395 | buffer_json_member_add_double(wb, "completion", s->stream.replication.completion); |
| 396 | buffer_json_member_add_uint64(wb, "instances", s->stream.replication.instances); |
| 397 | } |
| 398 | buffer_json_object_close(wb); // replication |
| 399 | |
| 400 | buffer_json_member_add_object(wb, "destination"); |
| 401 | { |
| 402 | char buf[1024 + 1]; |
| 403 | snprintfz(buf, sizeof(buf) - 1, "[%s]:%d%s", s->stream.peers.local.ip, s->stream.peers.local.port, s->stream.ssl ? ":SSL" : ""); |
| 404 | buffer_json_member_add_string(wb, "local", buf); |
| 405 | |
| 406 | snprintfz(buf, sizeof(buf) - 1, "[%s]:%d%s", s->stream.peers.peer.ip, s->stream.peers.peer.port, s->stream.ssl ? ":SSL" : ""); |
| 407 | buffer_json_member_add_string(wb, "remote", buf); |
| 408 | |
| 409 | stream_capabilities_to_json_array(wb, s->stream.capabilities, "capabilities"); |
| 410 | |
| 411 | buffer_json_member_add_object(wb, "traffic"); |
| 412 | { |
| 413 | buffer_json_member_add_boolean(wb, "compression", s->stream.compression); |
| 414 | buffer_json_member_add_uint64(wb, "data", s->stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_DATA]); |
| 415 | buffer_json_member_add_uint64(wb, "metadata", s->stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_METADATA]); |
| 416 | buffer_json_member_add_uint64(wb, "functions", s->stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_FUNCTIONS]); |
| 417 | buffer_json_member_add_uint64(wb, "replication", s->stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_REPLICATION]); |
| 418 | } |
| 419 | buffer_json_object_close(wb); // traffic |
| 420 | |
| 421 | buffer_json_member_add_array(wb, "parents"); |
| 422 | rrdhost_stream_parents_to_json(wb, s); |
| 423 | buffer_json_array_close(wb); // parents |
| 424 | |
| 425 | rrdhost_stream_path_to_json(wb, s->host, STREAM_PATH_JSON_MEMBER, false); |
| 426 | } |
| 427 | buffer_json_object_close(wb); // destination |
| 428 | } |
| 429 | buffer_json_object_close(wb); // streaming |
| 430 | } |
| 431 | |
| 432 | void agent_capabilities_to_json(BUFFER *wb, RRDHOST *host, const char *key) { |
| 433 | buffer_json_member_add_array(wb, key); |
| 434 | |
| 435 | struct capability *capas = aclk_get_node_instance_capas(host); |
| 436 | for(struct capability *capa = capas; capa->name ;capa++) { |
| 437 | buffer_json_add_array_item_object(wb); |
| 438 | { |
| 439 | buffer_json_member_add_string(wb, "name", capa->name); |
| 440 | buffer_json_member_add_uint64(wb, "version", capa->version); |
| 441 | buffer_json_member_add_boolean(wb, "enabled", capa->enabled); |
| 442 | } |
| 443 | buffer_json_object_close(wb); |
| 444 | } |
| 445 | buffer_json_array_close(wb); |
| 446 | freez(capas); |
| 447 | } |
| 448 | |
| 449 | static inline void host_dyncfg_to_json_v2(BUFFER *wb, const char *key, RRDHOST_STATUS *s) { |
| 450 | buffer_json_member_add_object(wb, key); |
| 451 | { |
| 452 | buffer_json_member_add_string(wb, "status", rrdhost_dyncfg_status_to_string(s->dyncfg.status)); |
| 453 | } |
| 454 | buffer_json_object_close(wb); // health |
| 455 | |
| 456 | } |
| 457 | |
| 458 | static inline void rrdhost_health_to_json_v2(BUFFER *wb, const char *key, RRDHOST_STATUS *s) { |
| 459 | buffer_json_member_add_object(wb, key); |
| 460 | { |
| 461 | buffer_json_member_add_string(wb, "status", rrdhost_health_status_to_string(s->health.status)); |
| 462 | if (s->health.status == RRDHOST_HEALTH_STATUS_RUNNING || s->health.status == RRDHOST_HEALTH_STATUS_INITIALIZING) { |
| 463 | buffer_json_member_add_object(wb, "alerts"); |
| 464 | { |
| 465 | buffer_json_member_add_uint64(wb, "critical", s->health.alerts.critical); |
| 466 | buffer_json_member_add_uint64(wb, "warning", s->health.alerts.warning); |
| 467 | buffer_json_member_add_uint64(wb, "clear", s->health.alerts.clear); |
| 468 | buffer_json_member_add_uint64(wb, "undefined", s->health.alerts.undefined); |
| 469 | buffer_json_member_add_uint64(wb, "uninitialized", s->health.alerts.uninitialized); |
| 470 | } |
| 471 | buffer_json_object_close(wb); // alerts |
| 472 | } |
| 473 | } |
| 474 | buffer_json_object_close(wb); // health |
| 475 | } |
| 476 | |
| 477 | static void rrdcontext_to_json_v2_rrdhost(BUFFER *wb, RRDHOST *host, struct rrdcontext_to_json_v2_data *ctl, size_t node_id) { |
| 478 | buffer_json_add_array_item_object(wb); // this node |
| 479 | |
| 480 | if(ctl->options & CONTEXTS_OPTION_MCP) |
| 481 | buffer_json_node_add_v2_mcp(wb, host, node_id); |
| 482 | else |
| 483 | buffer_json_node_add_v2(wb, host, node_id, 0, |
| 484 | (ctl->mode & CONTEXTS_V2_AGENTS) && !(ctl->mode & CONTEXTS_V2_NODE_INSTANCES)); |
| 485 | |
| 486 | if(ctl->mode & (CONTEXTS_V2_NODES_INFO | CONTEXTS_V2_NODES_STREAM_PATH | CONTEXTS_V2_NODE_INSTANCES)) { |
| 487 | RRDHOST_STATUS s; |
| 488 | rrdhost_status(host, ctl->now, &s, RRDHOST_STATUS_ALL); |
| 489 | |
| 490 | if (ctl->mode & (CONTEXTS_V2_NODES_INFO | CONTEXTS_V2_NODES_STREAM_PATH)) { |
| 491 | buffer_json_member_add_string(wb, "v", rrdhost_program_version(host)); |
| 492 | |
| 493 | host_labels2json(host, wb, "labels"); |
| 494 | rrdhost_system_info_to_json_v2(wb, host->system_info); |
| 495 | |
| 496 | // created - the node is created but never connected to cloud |
| 497 | // unreachable - not currently connected |
| 498 | // stale - connected but not having live data |
| 499 | // reachable - connected with live data |
| 500 | // pruned - not connected for some time and has been removed |
| 501 | buffer_json_member_add_string(wb, "state", rrdhost_is_online(host) ? "reachable" : "stale"); |
| 502 | } |
| 503 | |
| 504 | if (ctl->mode & (CONTEXTS_V2_NODES_INFO)) { |
| 505 | rrdhost_health_to_json_v2(wb, "health", &s); |
| 506 | agent_capabilities_to_json(wb, host, "capabilities"); |
| 507 | } |
| 508 | |
| 509 | if (ctl->mode & (CONTEXTS_V2_NODES_STREAM_PATH)) { |
| 510 | rrdhost_stream_path_to_json(wb, host, STREAM_PATH_JSON_MEMBER, false); |
| 511 | } |
| 512 | |
| 513 | if (ctl->mode & (CONTEXTS_V2_NODE_INSTANCES)) { |
| 514 | buffer_json_member_add_array(wb, "instances"); |
| 515 | buffer_json_add_array_item_object(wb); // this instance |
| 516 | { |
| 517 | buffer_json_agent_status_id(wb, 0, 0); |
| 518 | |
| 519 | buffer_json_member_add_object(wb, "db"); |
| 520 | { |
| 521 | buffer_json_member_add_string(wb, "status", rrdhost_db_status_to_string(s.db.status)); |
| 522 | buffer_json_member_add_string(wb, "liveness", rrdhost_db_liveness_to_string(s.db.liveness)); |
| 523 | buffer_json_member_add_string(wb, "mode", rrd_memory_mode_name(s.db.mode)); |
| 524 | buffer_json_member_add_time_t_formatted(wb, "first_time", s.db.first_time_s, ctl->options & CONTEXTS_OPTION_RFC3339); |
| 525 | buffer_json_member_add_time_t_formatted(wb, "last_time", s.db.last_time_s, ctl->options & CONTEXTS_OPTION_RFC3339); |
| 526 | |
| 527 | buffer_json_member_add_uint64(wb, "metrics", s.db.metrics); |
| 528 | buffer_json_member_add_uint64(wb, "instances", s.db.instances); |
| 529 | buffer_json_member_add_uint64(wb, "contexts", s.db.contexts); |
| 530 | } |
| 531 | buffer_json_object_close(wb); |
| 532 | |
| 533 | rrdhost_receiver_to_json(wb, &s, "ingest", ctl->options); |
| 534 | rrdhost_sender_to_json(wb, &s, "stream", ctl->options); |
| 535 | |
| 536 | buffer_json_member_add_object(wb, "ml"); |
| 537 | buffer_json_member_add_string(wb, "status", rrdhost_ml_status_to_string(s.ml.status)); |
| 538 | buffer_json_member_add_string(wb, "type", rrdhost_ml_type_to_string(s.ml.type)); |
| 539 | if (s.ml.status == RRDHOST_ML_STATUS_RUNNING) { |
| 540 | buffer_json_member_add_object(wb, "metrics"); |
| 541 | { |
| 542 | buffer_json_member_add_uint64(wb, "anomalous", s.ml.metrics.anomalous); |
| 543 | buffer_json_member_add_uint64(wb, "normal", s.ml.metrics.normal); |
| 544 | buffer_json_member_add_uint64(wb, "trained", s.ml.metrics.trained); |
| 545 | buffer_json_member_add_uint64(wb, "pending", s.ml.metrics.pending); |
| 546 | buffer_json_member_add_uint64(wb, "silenced", s.ml.metrics.silenced); |
| 547 | } |
| 548 | buffer_json_object_close(wb); // metrics |
| 549 | } |
| 550 | buffer_json_object_close(wb); // ml |
| 551 | |
| 552 | rrdhost_health_to_json_v2(wb, "health", &s); |
| 553 | |
| 554 | host_functions2json(host, wb); // functions |
| 555 | agent_capabilities_to_json(wb, host, "capabilities"); |
| 556 | |
| 557 | host_dyncfg_to_json_v2(wb, "dyncfg", &s); |
| 558 | } |
| 559 | buffer_json_object_close(wb); // this instance |
| 560 | buffer_json_array_close(wb); // instances |
| 561 | } |
| 562 | } |
| 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 |
| 619 | // or the host does not have any contexts |
| 620 | return 0; // continue to next host |
| 621 | |
| 622 | struct rrdcontext_to_json_v2_data *ctl = data; |
| 623 | |
| 624 | if(ctl->window.enabled && !rrdhost_matches_window(host, ctl->window.after, ctl->window.before, ctl->now)) |
| 625 | // the host does not have data in the requested window |
| 626 | return 0; // continue to next host |
| 627 | |
| 628 | if(ctl->request->timeout_ms && now_monotonic_usec() > ctl->timings.received_ut + ctl->request->timeout_ms * USEC_PER_MS) |
| 629 | // timed out |
| 630 | return -2; // stop the query |
| 631 | |
| 632 | if(ctl->request->interrupt_callback && ctl->request->interrupt_callback(ctl->request->interrupt_callback_data)) |
| 633 | // interrupted |
| 634 | return -1; // stop the query |
| 635 | |
| 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( |
| 653 | host, ctl->request->scope_contexts, |
| 654 | ctl->contexts.scope_pattern, ctl->contexts.pattern, |
| 655 | rrdcontext_to_json_v2_add_context, queryable_host, ctl); |
| 656 | |
| 657 | if(unlikely(added < 0)) |
| 658 | return -1; // stop the query |
| 659 | |
| 660 | if(added) |
| 661 | host_matched = true; |
| 662 | } |
| 663 | else if(!host_matched && ctl->window.enabled) { |
| 664 | time_t first_time_s = host->retention.first_time_s; |
| 665 | time_t last_time_s = host->retention.last_time_s; |
| 666 | if(rrdhost_is_online(host)) |
| 667 | last_time_s = ctl->now; // if the host is online, use the current time as the last time |
| 668 | |
| 669 | if(query_matches_retention(ctl->window.after, ctl->window.before, first_time_s, last_time_s, 0)) |
| 670 | host_matched = true; |
| 671 | } |
| 672 | |
| 673 | if(!host_matched) |
| 674 | return 0; |
| 675 | |
| 676 | if(ctl->mode & CONTEXTS_V2_FUNCTIONS) { |
| 677 | struct function_v2_entry t = { |
| 678 | .used = 1, |
| 679 | .size = 1, |
| 680 | .node_ids = &ctl->nodes.ni, |
| 681 | .help = NULL, |
| 682 | .tags = NULL, |
| 683 | .access = HTTP_ACCESS_ALL, |
| 684 | .priority = RRDFUNCTIONS_PRIORITY_DEFAULT, |
| 685 | .version = RRDFUNCTIONS_VERSION_DEFAULT, |
| 686 | }; |
| 687 | host_functions_to_dict(host, ctl->functions.dict, &t, sizeof(t), &t.help, &t.tags, &t.access, &t.priority, &t.version); |
| 688 | } |
| 689 | |
| 690 | if(ctl->mode & (CONTEXTS_V2_NODES | CONTEXTS_V2_FUNCTIONS | CONTEXTS_V2_ALERTS)) { |
| 691 | struct contexts_v2_node t = { |
| 692 | .ni = ctl->nodes.ni++, |
| 693 | .host = host, |
| 694 | }; |
| 695 | |
| 696 | dictionary_set(ctl->nodes.dict, host->machine_guid, &t, sizeof(struct contexts_v2_node)); |
| 697 | } |
| 698 | |
| 699 | return 1; |
| 700 | } |
| 701 | |
| 702 | static void buffer_json_contexts_v2_mode_to_array(BUFFER *wb, const char *key, CONTEXTS_V2_MODE mode) { |
| 703 | buffer_json_member_add_array(wb, key); |
| 704 | |
| 705 | if(mode & CONTEXTS_V2_VERSIONS) |
| 706 | buffer_json_add_array_item_string(wb, "versions"); |
| 707 | |
| 708 | if(mode & CONTEXTS_V2_AGENTS) |
| 709 | buffer_json_add_array_item_string(wb, "agents"); |
| 710 | |
| 711 | if(mode & CONTEXTS_V2_AGENTS_INFO) |
| 712 | buffer_json_add_array_item_string(wb, "agents-info"); |
| 713 | |
| 714 | if(mode & CONTEXTS_V2_NODES) |
| 715 | buffer_json_add_array_item_string(wb, "nodes"); |
| 716 | |
| 717 | if(mode & CONTEXTS_V2_NODES_INFO) |
| 718 | buffer_json_add_array_item_string(wb, "nodes-info"); |
| 719 | |
| 720 | if(mode & CONTEXTS_V2_NODES_STREAM_PATH) |
| 721 | buffer_json_add_array_item_string(wb, "nodes-stream-path"); |
| 722 | |
| 723 | if(mode & CONTEXTS_V2_NODE_INSTANCES) |
| 724 | buffer_json_add_array_item_string(wb, "nodes-instances"); |
| 725 | |
| 726 | if(mode & CONTEXTS_V2_CONTEXTS) |
| 727 | buffer_json_add_array_item_string(wb, "contexts"); |
| 728 | |
| 729 | if(mode & CONTEXTS_V2_SEARCH) |
| 730 | buffer_json_add_array_item_string(wb, "search"); |
| 731 | |
| 732 | if(mode & CONTEXTS_V2_ALERTS) |
| 733 | buffer_json_add_array_item_string(wb, "alerts"); |
| 734 | |
| 735 | if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) |
| 736 | buffer_json_add_array_item_string(wb, "alert_transitions"); |
| 737 | |
| 738 | buffer_json_array_close(wb); |
| 739 | } |
| 740 | |
| 741 | void buffer_json_query_timings(BUFFER *wb, const char *key, struct query_timings *timings) { |
| 742 | timings->finished_ut = now_monotonic_usec(); |
| 743 | if(!timings->executed_ut) |
| 744 | timings->executed_ut = timings->finished_ut; |
| 745 | if(!timings->preprocessed_ut) |
| 746 | timings->preprocessed_ut = timings->received_ut; |
| 747 | buffer_json_member_add_object(wb, key); |
| 748 | buffer_json_member_add_double(wb, "prep_ms", (NETDATA_DOUBLE)(timings->preprocessed_ut - timings->received_ut) / USEC_PER_MS); |
| 749 | buffer_json_member_add_double(wb, "query_ms", (NETDATA_DOUBLE)(timings->executed_ut - timings->preprocessed_ut) / USEC_PER_MS); |
| 750 | buffer_json_member_add_double(wb, "output_ms", (NETDATA_DOUBLE)(timings->finished_ut - timings->executed_ut) / USEC_PER_MS); |
| 751 | buffer_json_member_add_double(wb, "total_ms", (NETDATA_DOUBLE)(timings->finished_ut - timings->received_ut) / USEC_PER_MS); |
| 752 | buffer_json_member_add_double(wb, "cloud_ms", (NETDATA_DOUBLE)(timings->finished_ut - timings->received_ut) / USEC_PER_MS); |
| 753 | buffer_json_object_close(wb); |
| 754 | } |
| 755 | |
| 756 | void buffer_json_cloud_timings(BUFFER *wb, const char *key, struct query_timings *timings) { |
| 757 | if(!timings->finished_ut) |
| 758 | timings->finished_ut = now_monotonic_usec(); |
| 759 | |
| 760 | buffer_json_member_add_object(wb, key); |
| 761 | buffer_json_member_add_double(wb, "routing_ms", 0.0); |
| 762 | buffer_json_member_add_double(wb, "node_max_ms", 0.0); |
| 763 | buffer_json_member_add_double(wb, "total_ms", (NETDATA_DOUBLE)(timings->finished_ut - timings->received_ut) / USEC_PER_MS); |
| 764 | buffer_json_object_close(wb); |
| 765 | } |
| 766 | |
| 767 | static void functions_insert_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 768 | struct function_v2_entry *t = value; |
| 769 | |
| 770 | // it is initialized with a static reference - we need to mallocz() the array |
| 771 | size_t *v = t->node_ids; |
| 772 | t->node_ids = mallocz(sizeof(size_t)); |
| 773 | *t->node_ids = *v; |
| 774 | t->size = 1; |
| 775 | t->used = 1; |
| 776 | } |
| 777 | |
| 778 | static bool functions_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data __maybe_unused) { |
| 779 | struct function_v2_entry *t = old_value, *n = new_value; |
| 780 | size_t *v = n->node_ids; |
| 781 | |
| 782 | if(t->used >= t->size) { |
| 783 | t->node_ids = reallocz(t->node_ids, t->size * 2 * sizeof(size_t)); |
| 784 | t->size *= 2; |
| 785 | } |
| 786 | |
| 787 | t->node_ids[t->used++] = *v; |
| 788 | |
| 789 | return true; |
| 790 | } |
| 791 | |
| 792 | static void functions_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 793 | struct function_v2_entry *t = value; |
| 794 | freez(t->node_ids); |
| 795 | } |
| 796 | |
| 797 | static void contexts_cleanup(struct context_v2_entry *n) { |
| 798 | string_freez(n->title); |
| 799 | string_freez(n->units); |
| 800 | string_freez(n->family); |
| 801 | |
| 802 | dictionary_destroy(n->instances_dict); |
| 803 | dictionary_destroy(n->dimensions_dict); |
| 804 | rrdlabels_aggregated_destroy(n->labels_aggregated); |
| 805 | |
| 806 | // Clean up n's search results |
| 807 | dictionary_destroy(n->matched_instances); |
| 808 | dictionary_destroy(n->matched_dimensions); |
| 809 | rrdlabels_aggregated_destroy(n->matched_labels); |
| 810 | } |
| 811 | |
| 812 | static bool contexts_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data __maybe_unused) { |
| 813 | struct rrdcontext_to_json_v2_data *ctl = data; |
| 814 | struct context_v2_entry *o = old_value; |
| 815 | struct context_v2_entry *n = new_value; |
| 816 | |
| 817 | o->count++; |
| 818 | |
| 819 | o->flags |= n->flags; |
| 820 | o->nodes += n->nodes; |
| 821 | o->instances += n->instances; |
| 822 | |
| 823 | if (ctl->options & CONTEXTS_OPTION_TITLES) { |
| 824 | if(o->title != n->title) { |
| 825 | if((o->flags & RRD_FLAG_COLLECTED) && !(n->flags & RRD_FLAG_COLLECTED)) |
| 826 | // keep old |
| 827 | ; |
| 828 | else if(!(o->flags & RRD_FLAG_COLLECTED) && (n->flags & RRD_FLAG_COLLECTED)) { |
| 829 | // keep new |
| 830 | SWAP(o->title, n->title); |
| 831 | } |
| 832 | else { |
| 833 | // merge |
| 834 | STRING *old_title = o->title; |
| 835 | o->title = string_2way_merge(o->title, n->title); |
| 836 | string_freez(old_title); |
| 837 | // n->title will be freed below |
| 838 | } |
| 839 | } |
| 840 | } |
| 841 | |
| 842 | if (ctl->options & CONTEXTS_OPTION_FAMILY) { |
| 843 | if(o->family != n->family) { |
| 844 | if((o->flags & RRD_FLAG_COLLECTED) && !(n->flags & RRD_FLAG_COLLECTED)) |
| 845 | // keep old |
| 846 | ; |
| 847 | else if(!(o->flags & RRD_FLAG_COLLECTED) && (n->flags & RRD_FLAG_COLLECTED)) { |
| 848 | // keep new |
| 849 | SWAP(o->family, n->family); |
| 850 | } |
| 851 | else { |
| 852 | // merge |
| 853 | STRING *old_family = o->family; |
| 854 | o->family = string_2way_merge(o->family, n->family); |
| 855 | string_freez(old_family); |
| 856 | // n->family will be freed below |
| 857 | } |
| 858 | } |
| 859 | } |
| 860 | |
| 861 | if (ctl->options & CONTEXTS_OPTION_UNITS) { |
| 862 | if(o->units != n->units) { |
| 863 | if((o->flags & RRD_FLAG_COLLECTED) && !(n->flags & RRD_FLAG_COLLECTED)) |
| 864 | // keep old |
| 865 | ; |
| 866 | else if(!(o->flags & RRD_FLAG_COLLECTED) && (n->flags & RRD_FLAG_COLLECTED)) { |
| 867 | // keep new |
| 868 | SWAP(o->units, n->units); |
| 869 | } |
| 870 | else { |
| 871 | // keep old |
| 872 | ; |
| 873 | } |
| 874 | } |
| 875 | } |
| 876 | |
| 877 | if (ctl->options & CONTEXTS_OPTION_PRIORITIES) { |
| 878 | if(o->priority != n->priority) { |
| 879 | if((o->flags & RRD_FLAG_COLLECTED) && !(n->flags & RRD_FLAG_COLLECTED)) |
| 880 | // keep o |
| 881 | ; |
| 882 | else if(!(o->flags & RRD_FLAG_COLLECTED) && (n->flags & RRD_FLAG_COLLECTED)) |
| 883 | // keep n |
| 884 | o->priority = n->priority; |
| 885 | else |
| 886 | // keep the min |
| 887 | o->priority = MIN(o->priority, n->priority); |
| 888 | } |
| 889 | } |
| 890 | |
| 891 | if (ctl->options & CONTEXTS_OPTION_RETENTION) { |
| 892 | if(o->first_time_s && n->first_time_s) |
| 893 | o->first_time_s = MIN(o->first_time_s, n->first_time_s); |
| 894 | else if(!o->first_time_s) |
| 895 | o->first_time_s = n->first_time_s; |
| 896 | |
| 897 | if(o->last_time_s && n->last_time_s) |
| 898 | o->last_time_s = MAX(o->last_time_s, n->last_time_s); |
| 899 | else if(!o->last_time_s) |
| 900 | o->last_time_s = n->last_time_s; |
| 901 | } |
| 902 | |
| 903 | if (ctl->mode & CONTEXTS_V2_SEARCH) { |
| 904 | // Merge search results |
| 905 | o->matched_types |= n->matched_types; |
| 906 | |
| 907 | // For search result dictionaries, we need to merge them |
| 908 | if(n->matched_instances && !o->matched_instances) { |
| 909 | SWAP(o->matched_instances, n->matched_instances); |
| 910 | } |
| 911 | else if(n->matched_instances && o->matched_instances) { |
| 912 | // Merge entries from n to o |
| 913 | void *entry; |
| 914 | dfe_start_read(n->matched_instances, entry) { |
| 915 | dictionary_set(o->matched_instances, entry_dfe.name, NULL, 0); |
| 916 | } |
| 917 | dfe_done(entry); |
| 918 | } |
| 919 | |
| 920 | if(n->matched_dimensions && !o->matched_dimensions) { |
| 921 | SWAP(o->matched_dimensions, n->matched_dimensions); |
| 922 | } |
| 923 | else if(n->matched_dimensions && o->matched_dimensions) { |
| 924 | // Merge entries from n to o |
| 925 | void *entry; |
| 926 | dfe_start_read(n->matched_dimensions, entry) { |
| 927 | dictionary_set(o->matched_dimensions, entry_dfe.name, NULL, 0); |
| 928 | } |
| 929 | dfe_done(entry); |
| 930 | } |
| 931 | |
| 932 | if(n->matched_labels && !o->matched_labels) { |
| 933 | SWAP(o->matched_labels, n->matched_labels); |
| 934 | } |
| 935 | else if(n->matched_labels && o->matched_labels) { |
| 936 | // Merge n into o |
| 937 | rrdlabels_aggregated_merge(o->matched_labels, n->matched_labels); |
| 938 | } |
| 939 | } |
| 940 | |
| 941 | contexts_cleanup(n); |
| 942 | |
| 943 | // for the react callback to use |
| 944 | o->rc = n->rc; |
| 945 | |
| 946 | return true; |
| 947 | } |
| 948 | |
| 949 | static void contexts_react_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { |
| 950 | struct context_v2_entry *t = value; |
| 951 | struct rrdcontext_to_json_v2_data *ctl = data; |
| 952 | |
| 953 | // Only populate dictionaries if they exist (meaning the options are enabled) |
| 954 | if(!(ctl->mode & CONTEXTS_V2_CONTEXTS) || !(ctl->options & (CONTEXTS_OPTION_INSTANCES | CONTEXTS_OPTION_DIMENSIONS | CONTEXTS_OPTION_LABELS))) |
| 955 | return; |
| 956 | |
| 957 | // Initialize dictionaries for the new features if the corresponding options are set |
| 958 | if(ctl->options & CONTEXTS_OPTION_INSTANCES && !t->instances_dict) { |
| 959 | t->instances_dict = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, 0); |
| 960 | } |
| 961 | |
| 962 | if(ctl->options & CONTEXTS_OPTION_DIMENSIONS && !t->dimensions_dict) { |
| 963 | t->dimensions_dict = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, 0); |
| 964 | } |
| 965 | |
| 966 | if(ctl->options & CONTEXTS_OPTION_LABELS && !t->labels_aggregated) { |
| 967 | t->labels_aggregated = rrdlabels_aggregated_create(); |
| 968 | } |
| 969 | |
| 970 | RRDCONTEXT *rc = t->rc; |
| 971 | |
| 972 | // Collect instances, dimensions, and labels if requested |
| 973 | RRDINSTANCE *ri; |
| 974 | dfe_start_read(rc->rrdinstances, ri) { |
| 975 | if(ctl->window.enabled && !query_matches_retention(ctl->window.after, ctl->window.before, ri->first_time_s, (ri->flags & RRD_FLAG_COLLECTED) ? ctl->now : ri->last_time_s, (time_t)ri->update_every_s)) |
| 976 | continue; |
| 977 | |
| 978 | // Add instance name to instances dictionary |
| 979 | if(t->instances_dict) { |
| 980 | dictionary_set(t->instances_dict, string2str(ri->name), NULL, 0); |
| 981 | } |
| 982 | |
| 983 | // Collect dimensions from this instance |
| 984 | if(t->dimensions_dict) { |
| 985 | RRDMETRIC *rm; |
| 986 | dfe_start_read(ri->rrdmetrics, rm) { |
| 987 | if(ctl->window.enabled && !query_matches_retention(ctl->window.after, ctl->window.before, rm->first_time_s, (rm->flags & RRD_FLAG_COLLECTED) ? ctl->now : rm->last_time_s, (time_t)ri->update_every_s)) |
| 988 | continue; |
| 989 | |
| 990 | dictionary_set(t->dimensions_dict, string2str(rm->name), NULL, 0); |
| 991 | } |
| 992 | dfe_done(rm); |
| 993 | } |
| 994 | |
| 995 | // Collect labels from this instance |
| 996 | if(t->labels_aggregated) { |
| 997 | RRDLABELS *labels = rrdinstance_labels(ri); |
| 998 | rrdlabels_aggregated_add_from_rrdlabels(t->labels_aggregated, labels); |
| 999 | } |
| 1000 | } |
| 1001 | dfe_done(ri); |
| 1002 | } |
| 1003 | |
| 1004 | static void contexts_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 1005 | struct context_v2_entry *z = value; |
| 1006 | contexts_cleanup(z); |
| 1007 | } |
| 1008 | |
| 1009 | static void contexts_v2_search_results_to_json(BUFFER *wb, struct rrdcontext_to_json_v2_data *ctl) { |
| 1010 | size_t contexts_count = 0; |
| 1011 | size_t contexts_limit = ctl->request->cardinality_limit; |
| 1012 | size_t total_contexts = dictionary_entries(ctl->contexts.dict); |
| 1013 | |
| 1014 | // Calculate per-context item limit: MIN(cardinality_limit/total_contexts, 3) |
| 1015 | // But use the contexts that will be shown, not total |
| 1016 | size_t contexts_to_show = (contexts_limit && total_contexts > contexts_limit) ? contexts_limit : total_contexts; |
| 1017 | size_t per_context_limit = 3; // Default minimum |
| 1018 | if (contexts_limit && contexts_to_show > 0) { |
| 1019 | size_t calculated_limit = contexts_limit / contexts_to_show; |
| 1020 | if (calculated_limit > per_context_limit) |
| 1021 | per_context_limit = calculated_limit; |
| 1022 | } |
| 1023 | |
| 1024 | buffer_json_member_add_object(wb, "contexts"); |
| 1025 | |
| 1026 | struct context_v2_entry *z; |
| 1027 | dfe_start_read(ctl->contexts.dict, z) { |
| 1028 | // Check if we've reached the limit |
| 1029 | if (contexts_limit && contexts_count >= contexts_limit) { |
| 1030 | // Add a special entry indicating truncation |
| 1031 | buffer_json_member_add_object(wb, "__truncated__"); |
| 1032 | buffer_json_member_add_uint64(wb, "total_contexts", total_contexts); |
| 1033 | buffer_json_member_add_uint64(wb, "returned", contexts_count); |
| 1034 | buffer_json_member_add_uint64(wb, "remaining", total_contexts - contexts_count); |
| 1035 | buffer_json_object_close(wb); |
| 1036 | break; |
| 1037 | } |
| 1038 | |
| 1039 | buffer_json_member_add_object(wb, string2str(z->id)); |
| 1040 | { |
| 1041 | // Always show title, family, units in search results for context |
| 1042 | if (z->matched_types & SEARCH_MATCH_CONTEXT_TITLE) |
| 1043 | buffer_json_member_add_string(wb, "title", string2str(z->title)); |
| 1044 | |
| 1045 | if (z->matched_types & SEARCH_MATCH_CONTEXT_FAMILY) |
| 1046 | buffer_json_member_add_string(wb, "family", string2str(z->family)); |
| 1047 | |
| 1048 | if (z->matched_types & SEARCH_MATCH_CONTEXT_UNITS) |
| 1049 | buffer_json_member_add_string(wb, "units", string2str(z->units)); |
| 1050 | |
| 1051 | // Output what matched as an array |
| 1052 | if (!(ctl->options & CONTEXTS_OPTION_MCP)) { |
| 1053 | buffer_json_member_add_array(wb, "matched"); |
| 1054 | if (z->matched_types & SEARCH_MATCH_CONTEXT_ID) |
| 1055 | buffer_json_add_array_item_string(wb, "id"); |
| 1056 | if (z->matched_types & SEARCH_MATCH_CONTEXT_TITLE) |
| 1057 | buffer_json_add_array_item_string(wb, "title"); |
| 1058 | if (z->matched_types & SEARCH_MATCH_CONTEXT_UNITS) |
| 1059 | buffer_json_add_array_item_string(wb, "units"); |
| 1060 | if (z->matched_types & SEARCH_MATCH_CONTEXT_FAMILY) |
| 1061 | buffer_json_add_array_item_string(wb, "families"); |
| 1062 | if (z->matched_types & SEARCH_MATCH_INSTANCE) |
| 1063 | buffer_json_add_array_item_string(wb, "instances"); |
| 1064 | if (z->matched_types & SEARCH_MATCH_DIMENSION) |
| 1065 | buffer_json_add_array_item_string(wb, "dimensions"); |
| 1066 | if (z->matched_types & SEARCH_MATCH_LABEL) |
| 1067 | buffer_json_add_array_item_string(wb, "labels"); |
| 1068 | buffer_json_array_close(wb); |
| 1069 | } |
| 1070 | |
| 1071 | // Add instances array if any matched |
| 1072 | if(z->matched_instances && dictionary_entries(z->matched_instances) > 0) { |
| 1073 | buffer_json_member_add_array(wb, "instances"); |
| 1074 | void *entry; |
| 1075 | size_t count = 0; |
| 1076 | size_t total = dictionary_entries(z->matched_instances); |
| 1077 | |
| 1078 | dfe_start_read(z->matched_instances, entry) { |
| 1079 | if (per_context_limit && total > per_context_limit && count >= per_context_limit - 1) { |
| 1080 | char msg[100]; |
| 1081 | snprintf(msg, sizeof(msg), "... %zu instances more", total - count); |
| 1082 | buffer_json_add_array_item_string(wb, msg); |
| 1083 | break; |
| 1084 | } |
| 1085 | buffer_json_add_array_item_string(wb, entry_dfe.name); |
| 1086 | count++; |
| 1087 | } |
| 1088 | dfe_done(entry); |
| 1089 | buffer_json_array_close(wb); |
| 1090 | } |
| 1091 | |
| 1092 | // Add dimensions array if any matched |
| 1093 | if(z->matched_dimensions && dictionary_entries(z->matched_dimensions) > 0) { |
| 1094 | buffer_json_member_add_array(wb, "dimensions"); |
| 1095 | void *entry; |
| 1096 | size_t count = 0; |
| 1097 | size_t total = dictionary_entries(z->matched_dimensions); |
| 1098 | |
| 1099 | dfe_start_read(z->matched_dimensions, entry) { |
| 1100 | if (per_context_limit && total > per_context_limit && count >= per_context_limit - 1) { |
| 1101 | char msg[100]; |
| 1102 | snprintf(msg, sizeof(msg), "... %zu dimensions more", total - count); |
| 1103 | buffer_json_add_array_item_string(wb, msg); |
| 1104 | break; |
| 1105 | } |
| 1106 | buffer_json_add_array_item_string(wb, entry_dfe.name); |
| 1107 | count++; |
| 1108 | } |
| 1109 | dfe_done(entry); |
| 1110 | buffer_json_array_close(wb); |
| 1111 | } |
| 1112 | |
| 1113 | // Add labels if any matched |
| 1114 | if(z->matched_labels) { |
| 1115 | rrdlabels_aggregated_to_buffer_json( |
| 1116 | z->matched_labels, wb, "labels", per_context_limit); |
| 1117 | } |
| 1118 | } |
| 1119 | buffer_json_object_close(wb); |
| 1120 | |
| 1121 | contexts_count++; |
| 1122 | } |
| 1123 | dfe_done(z); |
| 1124 | |
| 1125 | buffer_json_object_close(wb); // contexts |
| 1126 | |
| 1127 | // Add info about cardinality limit if it was reached |
| 1128 | if (contexts_limit && total_contexts > contexts_limit && (ctl->options & CONTEXTS_OPTION_MCP)) { |
| 1129 | buffer_json_member_add_string(wb, "info", "Cardinality limit reached. Use cardinality_limit parameter to see more results."); |
| 1130 | } |
| 1131 | } |
| 1132 | |
| 1133 | static void contexts_v2_contexts_to_json(BUFFER *wb, struct rrdcontext_to_json_v2_data *ctl) { |
| 1134 | size_t contexts_count = 0; |
| 1135 | size_t contexts_limit = ctl->request->cardinality_limit; |
| 1136 | size_t total_contexts = dictionary_entries(ctl->contexts.dict); |
| 1137 | |
| 1138 | bool contexts_is_object = false; |
| 1139 | |
| 1140 | // If we have more contexts than the limit and MCP option is set, use categorized output |
| 1141 | if (contexts_limit && total_contexts > contexts_limit && (ctl->options & CONTEXTS_OPTION_MCP)) { |
| 1142 | buffer_json_member_add_object(wb, "contexts"); |
| 1143 | rrdcontext_categorize_and_output(wb, ctl->contexts.dict, contexts_limit); |
| 1144 | buffer_json_object_close(wb); |
| 1145 | if (ctl->options & CONTEXTS_OPTION_MCP) { |
| 1146 | buffer_json_member_add_string(wb, "info", MCP_INFO_TOO_MANY_CONTEXTS_GROUPED_IN_CATEGORIES); |
| 1147 | } |
| 1148 | } else { |
| 1149 | if (ctl->options & (CONTEXTS_OPTION_TITLES | CONTEXTS_OPTION_FAMILY | CONTEXTS_OPTION_UNITS | |
| 1150 | CONTEXTS_OPTION_PRIORITIES | CONTEXTS_OPTION_RETENTION | CONTEXTS_OPTION_LIVENESS | |
| 1151 | CONTEXTS_OPTION_DIMENSIONS | CONTEXTS_OPTION_LABELS | CONTEXTS_OPTION_INSTANCES)) { |
| 1152 | contexts_is_object = true; |
| 1153 | buffer_json_member_add_object(wb, "contexts"); |
| 1154 | } else |
| 1155 | buffer_json_member_add_array(wb, "contexts"); |
| 1156 | |
| 1157 | struct context_v2_entry *z; |
| 1158 | dfe_start_read(ctl->contexts.dict, z) { |
| 1159 | // Check if we've reached the limit |
| 1160 | if (contexts_limit && contexts_count >= contexts_limit) { |
| 1161 | // Add a special entry indicating truncation |
| 1162 | if (contexts_is_object) { |
| 1163 | buffer_json_member_add_object(wb, "__truncated__"); |
| 1164 | buffer_json_member_add_uint64(wb, "total_contexts", total_contexts); |
| 1165 | buffer_json_member_add_uint64(wb, "returned", contexts_count); |
| 1166 | buffer_json_member_add_uint64(wb, "remaining", total_contexts - contexts_count); |
| 1167 | buffer_json_object_close(wb); |
| 1168 | } else { |
| 1169 | char msg[100]; |
| 1170 | snprintf(msg, sizeof(msg), "... %zu contexts more", total_contexts - contexts_count); |
| 1171 | buffer_json_add_array_item_string(wb, msg); |
| 1172 | } |
| 1173 | break; |
| 1174 | } |
| 1175 | |
| 1176 | bool collected = z->flags & RRD_FLAG_COLLECTED; |
| 1177 | |
| 1178 | if (contexts_is_object) { |
| 1179 | buffer_json_member_add_object(wb, string2str(z->id)); |
| 1180 | { |
| 1181 | if (ctl->options & CONTEXTS_OPTION_TITLES) |
| 1182 | buffer_json_member_add_string(wb, "title", string2str(z->title)); |
| 1183 | |
| 1184 | if (ctl->options & CONTEXTS_OPTION_FAMILY) |
| 1185 | buffer_json_member_add_string(wb, "family", string2str(z->family)); |
| 1186 | |
| 1187 | if (ctl->options & CONTEXTS_OPTION_UNITS) |
| 1188 | buffer_json_member_add_string(wb, "units", string2str(z->units)); |
| 1189 | |
| 1190 | if (ctl->options & CONTEXTS_OPTION_PRIORITIES) |
| 1191 | buffer_json_member_add_uint64(wb, "priority", z->priority); |
| 1192 | |
| 1193 | if (ctl->options & CONTEXTS_OPTION_RETENTION) { |
| 1194 | buffer_json_member_add_time_t_formatted(wb, "first_entry", z->first_time_s, ctl->options & CONTEXTS_OPTION_RFC3339); |
| 1195 | buffer_json_member_add_time_t_formatted(wb, "last_entry", collected ? ctl->now : z->last_time_s, ctl->options & CONTEXTS_OPTION_RFC3339); |
| 1196 | } |
| 1197 | |
| 1198 | if (ctl->options & CONTEXTS_OPTION_LIVENESS) |
| 1199 | buffer_json_member_add_boolean(wb, "live", collected); |
| 1200 | |
| 1201 | // Add dimensions sub-object if requested |
| 1202 | if ((ctl->options & CONTEXTS_OPTION_DIMENSIONS) && z->dimensions_dict) { |
| 1203 | buffer_json_member_add_array(wb, "dimensions"); |
| 1204 | void *entry; |
| 1205 | size_t count = 0; |
| 1206 | size_t total = dictionary_entries(z->dimensions_dict); |
| 1207 | size_t limit = ctl->request->cardinality_limit; |
| 1208 | |
| 1209 | dfe_start_read(z->dimensions_dict, entry) { |
| 1210 | if (limit && count >= limit - 1 && total > limit) { |
| 1211 | // Add remaining count message |
| 1212 | char msg[100]; |
| 1213 | snprintf(msg, sizeof(msg), "... %zu dimensions more", total - count); |
| 1214 | buffer_json_add_array_item_string(wb, msg); |
| 1215 | break; |
| 1216 | } |
| 1217 | buffer_json_add_array_item_string(wb, entry_dfe.name); |
| 1218 | count++; |
| 1219 | } |
| 1220 | dfe_done(entry); |
| 1221 | buffer_json_array_close(wb); |
| 1222 | } |
| 1223 | |
| 1224 | // Add labels sub-object if requested |
| 1225 | if ((ctl->options & CONTEXTS_OPTION_LABELS) && z->labels_aggregated) { |
| 1226 | rrdlabels_aggregated_to_buffer_json( |
| 1227 | z->labels_aggregated, wb, "labels", ctl->request->cardinality_limit); |
| 1228 | } |
| 1229 | |
| 1230 | // Add instances sub-object if requested |
| 1231 | if ((ctl->options & CONTEXTS_OPTION_INSTANCES) && z->instances_dict) { |
| 1232 | buffer_json_member_add_array(wb, "instances"); |
| 1233 | void *entry; |
| 1234 | size_t count = 0; |
| 1235 | size_t total = dictionary_entries(z->instances_dict); |
| 1236 | size_t limit = ctl->request->cardinality_limit; |
| 1237 | |
| 1238 | dfe_start_read(z->instances_dict, entry) { |
| 1239 | if (limit && count >= limit - 1 && total > limit) { |
| 1240 | // Add remaining count message |
| 1241 | char msg[100]; |
| 1242 | snprintf(msg, sizeof(msg), "... %zu instances more", total - count); |
| 1243 | buffer_json_add_array_item_string(wb, msg); |
| 1244 | break; |
| 1245 | } |
| 1246 | buffer_json_add_array_item_string(wb, entry_dfe.name); |
| 1247 | count++; |
| 1248 | } |
| 1249 | dfe_done(entry); |
| 1250 | buffer_json_array_close(wb); |
| 1251 | } |
| 1252 | } |
| 1253 | buffer_json_object_close(wb); |
| 1254 | } else { |
| 1255 | buffer_json_add_array_item_string(wb, string2str(z->id)); |
| 1256 | } |
| 1257 | |
| 1258 | contexts_count++; |
| 1259 | } |
| 1260 | dfe_done(z); |
| 1261 | |
| 1262 | if(contexts_is_object) { |
| 1263 | buffer_json_object_close(wb); // contexts |
| 1264 | |
| 1265 | if (ctl->options & CONTEXTS_OPTION_MCP) { |
| 1266 | buffer_json_member_add_string(wb, "info", MCP_INFO_CONTEXT_NEXT_STEPS); |
| 1267 | } |
| 1268 | } |
| 1269 | else { |
| 1270 | buffer_json_array_close(wb); |
| 1271 | |
| 1272 | if (ctl->options & CONTEXTS_OPTION_MCP) { |
| 1273 | buffer_json_member_add_string(wb, "info", MCP_INFO_CONTEXT_ARRAY_RESPONSE); |
| 1274 | } |
| 1275 | } |
| 1276 | } |
| 1277 | } |
| 1278 | |
| 1279 | int rrdcontext_to_json_v2(BUFFER *wb, struct api_v2_contexts_request *req, CONTEXTS_V2_MODE mode) { |
| 1280 | int resp = HTTP_RESP_OK; |
| 1281 | bool run = true; |
| 1282 | |
| 1283 | if(mode & CONTEXTS_V2_ALERTS) { |
| 1284 | req->options &= ~CONTEXTS_OPTION_CONFIGURATIONS; |
| 1285 | } |
| 1286 | |
| 1287 | if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { |
| 1288 | req->options &= ~CONTEXTS_OPTION_INSTANCES; |
| 1289 | } |
| 1290 | |
| 1291 | struct rrdcontext_to_json_v2_data ctl = { |
| 1292 | .wb = wb, |
| 1293 | .request = req, |
| 1294 | .mode = mode, |
| 1295 | .options = req->options, |
| 1296 | .versions = { 0 }, |
| 1297 | .nodes.scope_pattern = string_to_simple_pattern(req->scope_nodes), |
| 1298 | .nodes.pattern = string_to_simple_pattern(req->nodes), |
| 1299 | .contexts.pattern = string_to_simple_pattern(req->contexts), |
| 1300 | .contexts.scope_pattern = string_to_simple_pattern(req->scope_contexts), |
| 1301 | .q.pattern = string_to_simple_pattern_nocase_substring(req->q), |
| 1302 | .alerts.alert_name_pattern = string_to_simple_pattern(req->alerts.alert), |
| 1303 | .window = { |
| 1304 | .enabled = false, |
| 1305 | .relative = false, |
| 1306 | .after = req->after, |
| 1307 | .before = req->before, |
| 1308 | }, |
| 1309 | .timings = { |
| 1310 | .received_ut = now_monotonic_usec(), |
| 1311 | } |
| 1312 | }; |
| 1313 | |
| 1314 | bool debug = ctl.options & CONTEXTS_OPTION_DEBUG; |
| 1315 | |
| 1316 | // Initialize JSON keys based on options |
| 1317 | json_keys_init((ctl.options & CONTEXTS_OPTION_JSON_LONG_KEYS) ? JSON_KEYS_OPTION_LONG_KEYS : 0); |
| 1318 | |
| 1319 | if(mode & (CONTEXTS_V2_NODES | CONTEXTS_V2_FUNCTIONS | CONTEXTS_V2_ALERTS)) { |
| 1320 | ctl.nodes.dict = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, |
| 1321 | NULL, sizeof(struct contexts_v2_node)); |
| 1322 | } |
| 1323 | |
| 1324 | if(mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH)) { |
| 1325 | ctl.contexts.dict = dictionary_create_advanced( |
| 1326 | DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, NULL, |
| 1327 | sizeof(struct context_v2_entry)); |
| 1328 | |
| 1329 | dictionary_register_conflict_callback(ctl.contexts.dict, contexts_conflict_callback, &ctl); |
| 1330 | dictionary_register_react_callback(ctl.contexts.dict, contexts_react_callback, &ctl); |
| 1331 | dictionary_register_delete_callback(ctl.contexts.dict, contexts_delete_callback, &ctl); |
| 1332 | } |
| 1333 | |
| 1334 | if(mode & CONTEXTS_V2_FUNCTIONS) { |
| 1335 | ctl.functions.dict = dictionary_create_advanced( |
| 1336 | DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, NULL, |
| 1337 | sizeof(struct function_v2_entry)); |
| 1338 | |
| 1339 | dictionary_register_insert_callback(ctl.functions.dict, functions_insert_callback, &ctl); |
| 1340 | dictionary_register_conflict_callback(ctl.functions.dict, functions_conflict_callback, &ctl); |
| 1341 | dictionary_register_delete_callback(ctl.functions.dict, functions_delete_callback, &ctl); |
| 1342 | } |
| 1343 | |
| 1344 | if(mode & CONTEXTS_V2_ALERTS) { |
| 1345 | if(!rrdcontexts_v2_init_alert_dictionaries(&ctl, req)) { |
| 1346 | resp = HTTP_RESP_NOT_FOUND; |
| 1347 | goto cleanup; |
| 1348 | } |
| 1349 | } |
| 1350 | |
| 1351 | if(req->after || req->before) { |
| 1352 | ctl.window.relative = rrdr_relative_window_to_absolute_query( |
| 1353 | &ctl.window.after, &ctl.window.before, &ctl.now, false); |
| 1354 | |
| 1355 | ctl.window.enabled = !(mode & CONTEXTS_V2_ALERT_TRANSITIONS); |
| 1356 | } |
| 1357 | else |
| 1358 | ctl.now = now_realtime_sec(); |
| 1359 | |
| 1360 | buffer_json_initialize(wb, "\"", "\"", 0, true, |
| 1361 | ((req->options & CONTEXTS_OPTION_MINIFY) && !(req->options & CONTEXTS_OPTION_DEBUG)) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); |
| 1362 | |
| 1363 | if(!(req->options & CONTEXTS_OPTION_MCP)) |
| 1364 | buffer_json_member_add_uint64(wb, "api", 2); |
| 1365 | |
| 1366 | if(req->options & CONTEXTS_OPTION_DEBUG) { |
| 1367 | buffer_json_member_add_object(wb, "request"); |
| 1368 | { |
| 1369 | buffer_json_contexts_v2_mode_to_array(wb, "mode", mode); |
| 1370 | contexts_options_to_buffer_json_array(wb, "options", req->options); |
| 1371 | |
| 1372 | buffer_json_member_add_object(wb, "scope"); |
| 1373 | { |
| 1374 | buffer_json_member_add_string(wb, "scope_nodes", req->scope_nodes); |
| 1375 | if (mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS)) |
| 1376 | buffer_json_member_add_string(wb, "scope_contexts", req->scope_contexts); |
| 1377 | } |
| 1378 | buffer_json_object_close(wb); |
| 1379 | |
| 1380 | buffer_json_member_add_object(wb, "selectors"); |
| 1381 | { |
| 1382 | buffer_json_member_add_string(wb, "nodes", req->nodes); |
| 1383 | |
| 1384 | if (mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS)) |
| 1385 | buffer_json_member_add_string(wb, "contexts", req->contexts); |
| 1386 | |
| 1387 | if(mode & (CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) { |
| 1388 | buffer_json_member_add_object(wb, "alerts"); |
| 1389 | |
| 1390 | if(mode & CONTEXTS_V2_ALERTS) |
| 1391 | contexts_alerts_status_to_buffer_json_array(wb, "status", req->alerts.status); |
| 1392 | |
| 1393 | if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { |
| 1394 | buffer_json_member_add_string(wb, "context", req->contexts); |
| 1395 | buffer_json_member_add_uint64(wb, "anchor_gi", req->alerts.global_id_anchor); |
| 1396 | buffer_json_member_add_uint64(wb, "last", req->alerts.last); |
| 1397 | } |
| 1398 | |
| 1399 | buffer_json_member_add_string(wb, "alert", req->alerts.alert); |
| 1400 | buffer_json_member_add_string(wb, "transition", req->alerts.transition); |
| 1401 | buffer_json_object_close(wb); // alerts |
| 1402 | } |
| 1403 | } |
| 1404 | buffer_json_object_close(wb); // selectors |
| 1405 | |
| 1406 | buffer_json_member_add_object(wb, "filters"); |
| 1407 | { |
| 1408 | if (mode & CONTEXTS_V2_SEARCH) |
| 1409 | buffer_json_member_add_string(wb, "q", req->q); |
| 1410 | |
| 1411 | buffer_json_member_add_time_t_formatted(wb, "after", req->after, ctl.options & CONTEXTS_OPTION_RFC3339); |
| 1412 | buffer_json_member_add_time_t_formatted(wb, "before", req->before, ctl.options & CONTEXTS_OPTION_RFC3339); |
| 1413 | } |
| 1414 | buffer_json_object_close(wb); // filters |
| 1415 | |
| 1416 | if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { |
| 1417 | buffer_json_member_add_object(wb, "facets"); |
| 1418 | { |
| 1419 | for (int i = 0; i < ATF_TOTAL_ENTRIES; i++) { |
| 1420 | buffer_json_member_add_string(wb, alert_transition_facets[i].query_param, req->alerts.facets[i]); |
| 1421 | } |
| 1422 | } |
| 1423 | buffer_json_object_close(wb); // facets |
| 1424 | } |
| 1425 | } |
| 1426 | buffer_json_object_close(wb); |
| 1427 | } |
| 1428 | |
| 1429 | ssize_t ret = 0; |
| 1430 | if(run) |
| 1431 | ret = query_scope_foreach_host(ctl.nodes.scope_pattern, ctl.nodes.pattern, |
| 1432 | rrdcontext_to_json_v2_add_host, &ctl, |
| 1433 | &ctl.versions, ctl.q.host_node_id_str); |
| 1434 | |
| 1435 | if(unlikely(ret < 0)) { |
| 1436 | buffer_flush(wb); |
| 1437 | |
| 1438 | if(ret == -2) { |
| 1439 | buffer_strcat(wb, "query timeout"); |
| 1440 | resp = HTTP_RESP_GATEWAY_TIMEOUT; |
| 1441 | } |
| 1442 | else { |
| 1443 | buffer_strcat(wb, "query interrupted"); |
| 1444 | resp = HTTP_RESP_CLIENT_CLOSED_REQUEST; |
| 1445 | } |
| 1446 | goto cleanup; |
| 1447 | } |
| 1448 | |
| 1449 | ctl.timings.executed_ut = now_monotonic_usec(); |
| 1450 | |
| 1451 | if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { |
| 1452 | contexts_v2_alert_transitions_to_json(wb, &ctl, debug); |
| 1453 | } |
| 1454 | else { |
| 1455 | if (mode & CONTEXTS_V2_NODES) { |
| 1456 | buffer_json_member_add_array(wb, "nodes"); |
| 1457 | struct contexts_v2_node *t; |
| 1458 | dfe_start_read(ctl.nodes.dict, t) { |
| 1459 | rrdcontext_to_json_v2_rrdhost(wb, t->host, &ctl, t->ni); |
| 1460 | } |
| 1461 | dfe_done(t); |
| 1462 | buffer_json_array_close(wb); |
| 1463 | } |
| 1464 | |
| 1465 | if (mode & CONTEXTS_V2_FUNCTIONS) { |
| 1466 | buffer_json_member_add_array(wb, "functions"); |
| 1467 | { |
| 1468 | struct function_v2_entry *t; |
| 1469 | dfe_start_read(ctl.functions.dict, t) { |
| 1470 | buffer_json_add_array_item_object(wb); |
| 1471 | { |
| 1472 | const char *name = t_dfe.name ? strstr(t_dfe.name, RRDFUNCTIONS_VERSION_SEPARATOR) : NULL; |
| 1473 | if(name) |
| 1474 | name += sizeof(RRDFUNCTIONS_VERSION_SEPARATOR) - 1; |
| 1475 | else |
| 1476 | name = t_dfe.name; |
| 1477 | |
| 1478 | buffer_json_member_add_string(wb, "name", name); |
| 1479 | buffer_json_member_add_string(wb, "help", string2str(t->help)); |
| 1480 | |
| 1481 | if (!(ctl.options & CONTEXTS_OPTION_MCP)) { |
| 1482 | buffer_json_member_add_array(wb, "ni"); |
| 1483 | { |
| 1484 | for (size_t i = 0; i < t->used; i++) |
| 1485 | buffer_json_add_array_item_uint64(wb, t->node_ids[i]); |
| 1486 | } |
| 1487 | buffer_json_array_close(wb); |
| 1488 | |
| 1489 | buffer_json_member_add_uint64(wb, "priority", t->priority); |
| 1490 | buffer_json_member_add_uint64(wb, "version", t->version); |
| 1491 | } |
| 1492 | buffer_json_member_add_string(wb, "tags", string2str(t->tags)); |
| 1493 | http_access2buffer_json_array(wb, "access", t->access); |
| 1494 | } |
| 1495 | buffer_json_object_close(wb); |
| 1496 | } |
| 1497 | dfe_done(t); |
| 1498 | } |
| 1499 | buffer_json_array_close(wb); |
| 1500 | } |
| 1501 | |
| 1502 | if (mode & CONTEXTS_V2_SEARCH) { |
| 1503 | contexts_v2_search_results_to_json(wb, &ctl); |
| 1504 | } |
| 1505 | else if (mode & CONTEXTS_V2_CONTEXTS) { |
| 1506 | contexts_v2_contexts_to_json(wb, &ctl); |
| 1507 | } |
| 1508 | |
| 1509 | if (mode & CONTEXTS_V2_ALERTS) |
| 1510 | contexts_v2_alerts_to_json(wb, &ctl, debug); |
| 1511 | |
| 1512 | if (mode & CONTEXTS_V2_SEARCH) { |
| 1513 | buffer_json_member_add_object(wb, "searches"); |
| 1514 | { |
| 1515 | buffer_json_member_add_uint64(wb, "strings", ctl.q.fts.string_searches); |
| 1516 | buffer_json_member_add_uint64(wb, "char", ctl.q.fts.char_searches); |
| 1517 | buffer_json_member_add_uint64(wb, "total", ctl.q.fts.searches); |
| 1518 | } |
| 1519 | buffer_json_object_close(wb); |
| 1520 | } |
| 1521 | |
| 1522 | if (mode & CONTEXTS_V2_VERSIONS) |
| 1523 | version_hashes_api_v2(wb, &ctl.versions); |
| 1524 | |
| 1525 | if (mode & CONTEXTS_V2_AGENTS) |
| 1526 | buffer_json_agents_v2(wb, &ctl.timings, ctl.now, mode & (CONTEXTS_V2_AGENTS_INFO), true, ctl.options); |
| 1527 | } |
| 1528 | |
| 1529 | if(!(ctl.options & CONTEXTS_OPTION_MCP)) |
| 1530 | buffer_json_cloud_timings(wb, "timings", &ctl.timings); |
| 1531 | |
| 1532 | buffer_json_finalize(wb); |
| 1533 | |
| 1534 | cleanup: |
| 1535 | dictionary_destroy(ctl.nodes.dict); |
| 1536 | dictionary_destroy(ctl.contexts.dict); |
| 1537 | dictionary_destroy(ctl.functions.dict); |
| 1538 | rrdcontexts_v2_alerts_cleanup(&ctl); |
| 1539 | simple_pattern_free(ctl.nodes.scope_pattern); |
| 1540 | simple_pattern_free(ctl.nodes.pattern); |
| 1541 | simple_pattern_free(ctl.contexts.pattern); |
| 1542 | simple_pattern_free(ctl.contexts.scope_pattern); |
| 1543 | simple_pattern_free(ctl.q.pattern); |
| 1544 | simple_pattern_free(ctl.alerts.alert_name_pattern); |
| 1545 | |
| 1546 | json_keys_reset(); |
| 1547 | return resp; |
| 1548 | } |