master
c 1,548 lines 65.5 KB
Raw
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 }