| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "api_v2_calls.h" |
| 4 | |
| 5 | #define GROUP_BY_KEY_MAX_LENGTH 30 |
| 6 | static struct { |
| 7 | char group_by[GROUP_BY_KEY_MAX_LENGTH + 1]; |
| 8 | char aggregation[GROUP_BY_KEY_MAX_LENGTH + 1]; |
| 9 | char group_by_label[GROUP_BY_KEY_MAX_LENGTH + 1]; |
| 10 | } group_by_keys[MAX_QUERY_GROUP_BY_PASSES]; |
| 11 | |
| 12 | __attribute__((constructor)) void initialize_group_by_keys(void) { |
| 13 | for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { |
| 14 | snprintfz(group_by_keys[g].group_by, GROUP_BY_KEY_MAX_LENGTH, "group_by[%zu]", g); |
| 15 | snprintfz(group_by_keys[g].aggregation, GROUP_BY_KEY_MAX_LENGTH, "aggregation[%zu]", g); |
| 16 | snprintfz(group_by_keys[g].group_by_label, GROUP_BY_KEY_MAX_LENGTH, "group_by_label[%zu]", g); |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | static int api_v23_data_internal(RRDHOST *host __maybe_unused, struct web_client *w, char *url, size_t version) { |
| 21 | usec_t received_ut = now_monotonic_usec(); |
| 22 | |
| 23 | int ret = HTTP_RESP_BAD_REQUEST; |
| 24 | |
| 25 | buffer_flush(w->response.data); |
| 26 | |
| 27 | char *google_version = "0.6", |
| 28 | *google_reqId = "0", |
| 29 | *google_sig = "0", |
| 30 | *google_out = "json", |
| 31 | *responseHandler = NULL, |
| 32 | *outFileName = NULL; |
| 33 | |
| 34 | time_t last_timestamp_in_data = 0, google_timestamp = 0; |
| 35 | |
| 36 | char *scope_nodes = NULL; |
| 37 | char *scope_contexts = NULL; |
| 38 | char *scope_instances = NULL; |
| 39 | char *scope_labels = NULL; |
| 40 | char *scope_dimensions = NULL; |
| 41 | char *nodes = NULL; |
| 42 | char *contexts = NULL; |
| 43 | char *instances = NULL; |
| 44 | char *dimensions = NULL; |
| 45 | char *before_str = NULL; |
| 46 | char *after_str = NULL; |
| 47 | char *resampling_time_str = NULL; |
| 48 | char *points_str = NULL; |
| 49 | char *timeout_str = NULL; |
| 50 | char *labels = NULL; |
| 51 | char *alerts = NULL; |
| 52 | char *time_group_options = NULL; |
| 53 | char *tier_str = NULL; |
| 54 | char *cardinality_limit_str = NULL; |
| 55 | size_t tier = 0; |
| 56 | size_t cardinality_limit = 0; |
| 57 | RRDR_TIME_GROUPING time_group = RRDR_GROUPING_AVERAGE; |
| 58 | DATASOURCE_FORMAT format = DATASOURCE_JSON2; |
| 59 | RRDR_OPTIONS options = RRDR_OPTION_VIRTUAL_POINTS | RRDR_OPTION_JSON_WRAP | RRDR_OPTION_RETURN_JWAR; |
| 60 | |
| 61 | struct group_by_pass group_by[MAX_QUERY_GROUP_BY_PASSES] = { |
| 62 | { |
| 63 | .group_by = RRDR_GROUP_BY_DIMENSION, |
| 64 | .group_by_label = NULL, |
| 65 | .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, |
| 66 | }, |
| 67 | }; |
| 68 | |
| 69 | size_t group_by_idx = 0, group_by_label_idx = 0, aggregation_idx = 0; |
| 70 | |
| 71 | while(url) { |
| 72 | char *value = strsep_skip_consecutive_separators(&url, "&"); |
| 73 | if(!value || !*value) continue; |
| 74 | |
| 75 | char *name = strsep_skip_consecutive_separators(&value, "="); |
| 76 | if(!name || !*name) continue; |
| 77 | if(!value || !*value) continue; |
| 78 | |
| 79 | // name and value are now the parameters |
| 80 | // they are not null and not empty |
| 81 | |
| 82 | if(!strcmp(name, "scope_nodes")) scope_nodes = value; |
| 83 | else if(!strcmp(name, "scope_contexts")) scope_contexts = value; |
| 84 | else if(!strcmp(name, "scope_instances")) scope_instances = value; |
| 85 | else if(!strcmp(name, "scope_labels")) scope_labels = value; |
| 86 | else if(!strcmp(name, "scope_dimensions")) scope_dimensions = value; |
| 87 | else if(!strcmp(name, "nodes")) nodes = value; |
| 88 | else if(!strcmp(name, "contexts")) contexts = value; |
| 89 | else if(!strcmp(name, "instances")) instances = value; |
| 90 | else if(!strcmp(name, "dimensions")) dimensions = value; |
| 91 | else if(!strcmp(name, "labels")) labels = value; |
| 92 | else if(!strcmp(name, "alerts")) alerts = value; |
| 93 | else if(!strcmp(name, "after")) after_str = value; |
| 94 | else if(!strcmp(name, "before")) before_str = value; |
| 95 | else if(!strcmp(name, "points")) points_str = value; |
| 96 | else if(!strcmp(name, "timeout")) timeout_str = value; |
| 97 | else if(!strcmp(name, "group_by")) { |
| 98 | group_by[group_by_idx++].group_by = group_by_parse(value); |
| 99 | if(group_by_idx >= MAX_QUERY_GROUP_BY_PASSES) |
| 100 | group_by_idx = MAX_QUERY_GROUP_BY_PASSES - 1; |
| 101 | } |
| 102 | else if(!strcmp(name, "group_by_label")) { |
| 103 | group_by[group_by_label_idx++].group_by_label = value; |
| 104 | if(group_by_label_idx >= MAX_QUERY_GROUP_BY_PASSES) |
| 105 | group_by_label_idx = MAX_QUERY_GROUP_BY_PASSES - 1; |
| 106 | } |
| 107 | else if(!strcmp(name, "aggregation")) { |
| 108 | group_by[aggregation_idx++].aggregation = group_by_aggregate_function_parse(value); |
| 109 | if(aggregation_idx >= MAX_QUERY_GROUP_BY_PASSES) |
| 110 | aggregation_idx = MAX_QUERY_GROUP_BY_PASSES - 1; |
| 111 | } |
| 112 | else if(!strcmp(name, "format")) format = datasource_format_str_to_id(value); |
| 113 | else if(!strcmp(name, "options")) options |= rrdr_options_parse(value); |
| 114 | else if(!strcmp(name, "time_group")) time_group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); |
| 115 | else if(!strcmp(name, "time_group_options")) time_group_options = value; |
| 116 | else if(!strcmp(name, "time_resampling")) resampling_time_str = value; |
| 117 | else if(!strcmp(name, "tier")) tier_str = value; |
| 118 | else if(!strcmp(name, "cardinality_limit")) cardinality_limit_str = value; |
| 119 | else if(!strcmp(name, "callback")) responseHandler = value; |
| 120 | else if(!strcmp(name, "filename")) outFileName = value; |
| 121 | else if(!strcmp(name, "tqx")) { |
| 122 | // parse Google Visualization API options |
| 123 | // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source |
| 124 | char *tqx_name, *tqx_value; |
| 125 | |
| 126 | while(value) { |
| 127 | tqx_value = strsep_skip_consecutive_separators(&value, ";"); |
| 128 | if(!tqx_value || !*tqx_value) continue; |
| 129 | |
| 130 | tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); |
| 131 | if(!tqx_name || !*tqx_name) continue; |
| 132 | if(!tqx_value || !*tqx_value) continue; |
| 133 | |
| 134 | if(!strcmp(tqx_name, "version")) |
| 135 | google_version = tqx_value; |
| 136 | else if(!strcmp(tqx_name, "reqId")) |
| 137 | google_reqId = tqx_value; |
| 138 | else if(!strcmp(tqx_name, "sig")) { |
| 139 | google_sig = tqx_value; |
| 140 | google_timestamp = strtoul(google_sig, NULL, 0); |
| 141 | } |
| 142 | else if(!strcmp(tqx_name, "out")) { |
| 143 | google_out = tqx_value; |
| 144 | format = google_data_format_str_to_id(google_out); |
| 145 | } |
| 146 | else if(!strcmp(tqx_name, "responseHandler")) |
| 147 | responseHandler = tqx_value; |
| 148 | else if(!strcmp(tqx_name, "outFileName")) |
| 149 | outFileName = tqx_value; |
| 150 | } |
| 151 | } |
| 152 | else { |
| 153 | for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { |
| 154 | if(!strcmp(name, group_by_keys[g].group_by)) |
| 155 | group_by[g].group_by = group_by_parse(value); |
| 156 | else if(!strcmp(name, group_by_keys[g].group_by_label)) |
| 157 | group_by[g].group_by_label = value; |
| 158 | else if(!strcmp(name, group_by_keys[g].aggregation)) |
| 159 | group_by[g].aggregation = group_by_aggregate_function_parse(value); |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | // validate the google parameters given |
| 165 | fix_google_param(google_out); |
| 166 | fix_google_param(google_sig); |
| 167 | fix_google_param(google_reqId); |
| 168 | fix_google_param(google_version); |
| 169 | fix_google_param(responseHandler); |
| 170 | fix_google_param(outFileName); |
| 171 | |
| 172 | for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { |
| 173 | if (group_by[g].group_by_label && *group_by[g].group_by_label) |
| 174 | group_by[g].group_by |= RRDR_GROUP_BY_LABEL; |
| 175 | } |
| 176 | |
| 177 | if(group_by[0].group_by == RRDR_GROUP_BY_NONE) |
| 178 | group_by[0].group_by = RRDR_GROUP_BY_DIMENSION; |
| 179 | |
| 180 | for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { |
| 181 | if ((group_by[g].group_by != RRDR_GROUP_BY_NONE && !(group_by[g].group_by & RRDR_GROUP_BY_DIMENSION)) || |
| 182 | (options & RRDR_OPTION_PERCENTAGE)) { |
| 183 | options |= RRDR_OPTION_ABSOLUTE; |
| 184 | break; |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | if(options & RRDR_OPTION_DEBUG) |
| 189 | options &= ~RRDR_OPTION_MINIFY; |
| 190 | |
| 191 | if(tier_str && *tier_str) { |
| 192 | tier = str2ul(tier_str); |
| 193 | if(tier < nd_profile.storage_tiers) |
| 194 | options |= RRDR_OPTION_SELECTED_TIER; |
| 195 | else |
| 196 | tier = 0; |
| 197 | } |
| 198 | |
| 199 | if(cardinality_limit_str && *cardinality_limit_str) { |
| 200 | cardinality_limit = str2ul(cardinality_limit_str); |
| 201 | } |
| 202 | |
| 203 | time_t before = (before_str && *before_str)?str2l(before_str):0; |
| 204 | time_t after = (after_str && *after_str) ?str2l(after_str):-600; |
| 205 | size_t points = (points_str && *points_str)?str2u(points_str):0; |
| 206 | int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; |
| 207 | time_t resampling_time = (resampling_time_str && *resampling_time_str) ? str2l(resampling_time_str) : 0; |
| 208 | |
| 209 | QUERY_TARGET_REQUEST qtr = { |
| 210 | .version = version, |
| 211 | .scope_nodes = scope_nodes, |
| 212 | .scope_contexts = scope_contexts, |
| 213 | .scope_instances = scope_instances, |
| 214 | .scope_labels = scope_labels, |
| 215 | .scope_dimensions = scope_dimensions, |
| 216 | .after = after, |
| 217 | .before = before, |
| 218 | .host = NULL, |
| 219 | .st = NULL, |
| 220 | .nodes = nodes, |
| 221 | .contexts = contexts, |
| 222 | .instances = instances, |
| 223 | .dimensions = dimensions, |
| 224 | .alerts = alerts, |
| 225 | .timeout_ms = timeout, |
| 226 | .points = points, |
| 227 | .format = format, |
| 228 | .options = options, |
| 229 | .time_group_method = time_group, |
| 230 | .time_group_options = time_group_options, |
| 231 | .resampling_time = resampling_time, |
| 232 | .tier = tier, |
| 233 | .chart_label_key = NULL, |
| 234 | .labels = labels, |
| 235 | .query_source = QUERY_SOURCE_API_DATA, |
| 236 | .priority = STORAGE_PRIORITY_NORMAL, |
| 237 | .received_ut = received_ut, |
| 238 | .cardinality_limit = cardinality_limit, |
| 239 | |
| 240 | .interrupt_callback = web_client_interrupt_callback, |
| 241 | .interrupt_callback_data = w, |
| 242 | |
| 243 | .transaction = &w->transaction, |
| 244 | }; |
| 245 | |
| 246 | for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) |
| 247 | qtr.group_by[g] = group_by[g]; |
| 248 | |
| 249 | QUERY_TARGET *qt = query_target_create(&qtr); |
| 250 | ONEWAYALLOC *owa = NULL; |
| 251 | |
| 252 | if(!qt) { |
| 253 | buffer_sprintf(w->response.data, "Failed to prepare the query."); |
| 254 | ret = HTTP_RESP_INTERNAL_SERVER_ERROR; |
| 255 | goto cleanup; |
| 256 | } |
| 257 | |
| 258 | web_client_timeout_checkpoint_set(w, timeout); |
| 259 | if(web_client_timeout_checkpoint_and_check(w, NULL)) { |
| 260 | ret = w->response.code; |
| 261 | goto cleanup; |
| 262 | } |
| 263 | |
| 264 | if(outFileName && *outFileName) { |
| 265 | buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); |
| 266 | netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); |
| 267 | } |
| 268 | |
| 269 | if(format == DATASOURCE_DATATABLE_JSONP) { |
| 270 | if(responseHandler == NULL) |
| 271 | responseHandler = "google.visualization.Query.setResponse"; |
| 272 | |
| 273 | netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", |
| 274 | w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName |
| 275 | ); |
| 276 | |
| 277 | buffer_sprintf( |
| 278 | w->response.data, |
| 279 | "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", |
| 280 | responseHandler, |
| 281 | google_version, |
| 282 | google_reqId, |
| 283 | (int64_t)now_realtime_sec()); |
| 284 | } |
| 285 | else if(format == DATASOURCE_JSONP) { |
| 286 | if(responseHandler == NULL) |
| 287 | responseHandler = "callback"; |
| 288 | |
| 289 | buffer_strcat(w->response.data, responseHandler); |
| 290 | buffer_strcat(w->response.data, "("); |
| 291 | } |
| 292 | |
| 293 | owa = onewayalloc_create(0); |
| 294 | ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); |
| 295 | |
| 296 | if(format == DATASOURCE_DATATABLE_JSONP) { |
| 297 | if(google_timestamp < last_timestamp_in_data) |
| 298 | buffer_strcat(w->response.data, "});"); |
| 299 | |
| 300 | else { |
| 301 | // the client already has the latest data |
| 302 | buffer_flush(w->response.data); |
| 303 | buffer_sprintf(w->response.data, |
| 304 | "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", |
| 305 | responseHandler, google_version, google_reqId); |
| 306 | } |
| 307 | } |
| 308 | else if(format == DATASOURCE_JSONP) |
| 309 | buffer_strcat(w->response.data, ");"); |
| 310 | |
| 311 | if(qt->internal.relative) |
| 312 | buffer_no_cacheable(w->response.data); |
| 313 | else |
| 314 | buffer_cacheable(w->response.data); |
| 315 | |
| 316 | cleanup: |
| 317 | query_target_release(qt); |
| 318 | onewayalloc_destroy(owa); |
| 319 | return ret; |
| 320 | } |
| 321 | |
| 322 | int api_v2_data(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { |
| 323 | return api_v23_data_internal(host, w, url, 2); |
| 324 | } |
| 325 | |
| 326 | int api_v3_data(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { |
| 327 | return api_v23_data_internal(host, w, url, 3); |
| 328 | } |