diff --git a/modules/prometheus/README b/modules/prometheus/README index 3baf3c16d33..403e5b5d355 100644 --- a/modules/prometheus/README +++ b/modules/prometheus/README @@ -21,6 +21,8 @@ Prometheus Module 1.3.6. group_mode(int) 1.3.7. statistics(string) 1.3.8. labels(string) + 1.3.9. script_route(string) + 1.3.10. script_route_avp_result(string) 1.4. Exported Functions 1.5. Examples @@ -51,7 +53,9 @@ Prometheus Module 1.6. Set group_mode parameter 1.7. Set statistics parameter 1.8. Set statistics parameter - 1.9. Prometheus Scrape Config + 1.9. Set script_route parameter + 1.10. Set script_route parameter + 1.11. Prometheus Scrape Config Chapter 1. Admin Guide @@ -230,6 +234,65 @@ modparam("prometheus", "labels", "group: /^(.*)_(.*)$/\1:gateway=\"\2\"/ ") ... +1.3.9. script_route(string) + + Specifies the route name to be used to for adding custom + prometheus information. + + The default value is "" - no custom route called. + + Example 1.9. Set script_route parameter +... +modparam("prometheus", "script_route", "my_custom_prometheus_route") +... + +1.3.10. script_route_avp_result(string) + + Specifies the AVP spec name to be used to for returning custom + prometheus information. + + The AVP is expected to contain a JSON format, containing an + array of arrays of custom statistics to export to prometheus. + See section for the format itself. + + The default value is "" - no custom route called. + + Example 1.10. Set script_route parameter +... +modparam("prometheus", "script_route", "my_custom_prometheus_route") +modparam("prometheus", "script_route_avp_result", "$avp(prometheus_out_j +son)") +... +route[my_custom_prometheus_route] { + # the OUT JSON needs to contain an array of objects containing a + header and a values field + # the header field to contain the custom prometheus stats header + # the values field is an array itself, of name/value objects + # used for individual stats publishing + $avp(prometheus_out_json) = " +[ { + "header": "# TYPE opensips_cps gauge", + "values": [ + { + "name": "opensips_total_cps", + "value": 3 + } + ] + }, + { + "header": "# TYPE opensips_disabled_rtpengine gauge", + "values": [ + { + "name": "opensips_disabled_rtpengine", + "value": 0 + } + ] + }, +]"; + +} +... + 1.4. Exported Functions No function exported to be used from configuration file. @@ -242,7 +305,7 @@ modparam("prometheus", "labels", "group: /^(.*)_(.*)$/\1:gateway=\"\2\"/ config, indicating the IP and port you've configured the httpd module to listen on (default: 0.0.0.0:8888). - Example 1.9. Prometheus Scrape Config + Example 1.11. Prometheus Scrape Config scrape_configs: - job_name: opensips @@ -300,4 +363,4 @@ Chapter 3. Documentation Documentation Copyrights: - Copyright © 2021 www.opensips-solutions.com + Copyright © 2021 www.opensips-solutions.com diff --git a/modules/prometheus/doc/prometheus_admin.xml b/modules/prometheus/doc/prometheus_admin.xml index 5b2dfc0f5df..f64a1945956 100644 --- a/modules/prometheus/doc/prometheus_admin.xml +++ b/modules/prometheus/doc/prometheus_admin.xml @@ -262,6 +262,75 @@ modparam("prometheus", "labels", "group: /^(.*)_(.*)$/\1:gateway=\"\2\"/") + +
+ <varname>script_route</varname>(string) + + Specifies the route name to be used to for adding custom prometheus information. + + + The default value is "" - no custom route called. + + + Set <varname>script_route</varname> parameter + +... +modparam("prometheus", "script_route", "my_custom_prometheus_route") +... + + +
+ +
+ <varname>script_route_avp_result</varname>(string) + + Specifies the AVP spec name to be used to for returning custom prometheus information. + + + The AVP is expected to contain a JSON format, containing an array of arrays of custom statistics to export to prometheus. See section for the format itself. + + + The default value is "" - no custom route called. + + + Set <varname>script_route</varname> parameter + +... +modparam("prometheus", "script_route", "my_custom_prometheus_route") +modparam("prometheus", "script_route_avp_result", "$avp(prometheus_out_json)") +... +route[my_custom_prometheus_route] { + # the OUT JSON needs to contain an array of objects containing a header and a values field + # the header field to contain the custom prometheus stats header + # the values field is an array itself, of name/value objects + # used for individual stats publishing + $avp(prometheus_out_json) = " +[ { + "header": "# TYPE opensips_cps gauge", + "values": [ + { + "name": "opensips_total_cps", + "value": 3 + } + ] + }, + { + "header": "# TYPE opensips_disabled_rtpengine gauge", + "values": [ + { + "name": "opensips_disabled_rtpengine", + "value": 0 + } + ] + }, +]"; + +} +... + + +
+
diff --git a/modules/prometheus/prometheus.c b/modules/prometheus/prometheus.c index 0c32d844095..f12f7a1a9ea 100644 --- a/modules/prometheus/prometheus.c +++ b/modules/prometheus/prometheus.c @@ -55,6 +55,11 @@ str prom_grp_prefix = str_init(""); str prom_delimiter = str_init("_"); str prom_grp_label = str_init("group"); httpd_api_t prom_httpd_api; +str prometheus_script_route = {NULL, 0}; +struct script_route_ref *prometheus_route_ref = NULL; +char* prometheus_avp_param = NULL; +unsigned short prometheus_avp_type = 0; +int prometheus_avp_name = -1; static int prom_stats_param( modparam_t type, void* val); static int prom_labels_param( modparam_t type, void* val); @@ -69,6 +74,8 @@ static const param_export_t mi_params[] = { {"group_mode", INT_PARAM, &prom_grp_mode}, {"statistics", STR_PARAM|USE_FUNC_PARAM, &prom_stats_param}, {"labels", STR_PARAM|USE_FUNC_PARAM, &prom_labels_param}, + {"script_route", STR_PARAM, &prometheus_script_route}, + {"script_route_avp_result", STR_PARAM, &prometheus_avp_param}, {0,0,0} }; @@ -131,6 +138,8 @@ static int mod_init(void) { struct list_head *it; struct prom_stat *s; + str avp_s; + pv_spec_t avp_spec; prom_http_root.len = strlen(prom_http_root.s); prom_prefix.len = strlen(prom_prefix.s); @@ -138,6 +147,36 @@ static int mod_init(void) prom_grp_prefix.len = strlen(prom_grp_prefix.s); prom_grp_label.len = strlen(prom_grp_label.s); + if (prometheus_script_route.s) { + prometheus_script_route.len = strlen(prometheus_script_route.s); + + prometheus_route_ref = ref_script_route_by_name(prometheus_script_route.s, + sroutes->request, RT_NO , REQUEST_ROUTE, 0); + if ( !ref_script_route_is_valid(prometheus_route_ref) ) { + LM_ERR("Prometheus route <%s> not defined!\n", prometheus_script_route.s); + return -1; + } + + if (prometheus_avp_param && *prometheus_avp_param) { + avp_s.s = prometheus_avp_param; + avp_s.len = strlen(avp_s.s); + if (pv_parse_spec(&avp_s, &avp_spec)==0 + || avp_spec.type!=PVT_AVP) { + LM_ERR("malformed or non AVP %s AVP definition\n", prometheus_avp_param); + return -1; + } + + if(pv_get_avp_name(0, &avp_spec.pvp, &prometheus_avp_name, &prometheus_avp_type)!=0) + { + LM_ERR("[%s]- invalid AVP definition\n", prometheus_avp_param); + return -1; + } + } else { + LM_ERR("Prometheus route <%s> defined but no AVP result set!\n", prometheus_script_route.s); + return -1; + } + } + if (prom_grp_mode < PROM_GROUP_MODE_NONE || prom_grp_mode >= PROM_GROUP_MODE_INVALID) { LM_ERR("invalid group mode %d\n", prom_grp_mode); return -1; @@ -597,6 +636,135 @@ static inline int prom_push_stat(stat_var *stat, str *page, int max_len, } \ } while(0) +int process_extra_prometheus_entry(cJSON *obj,str *page, int max_len) +{ + cJSON *header,*values,*value, *name, *counter; + str val, cval; + int c; + + if (!obj) + return -1; + + if (obj->type != cJSON_Object) { + LM_ERR("Expecting a JSON object in the array but got %d \n",obj->type); + return -1; + } + + header = cJSON_GetObjectItem(obj, "header"); + values = cJSON_GetObjectItem(obj, "values"); + + if (!header || header->type != cJSON_String) { + LM_ERR("Invalid header provided \n"); + return -1; + } + + if (!values || values->type != cJSON_Array) { + LM_ERR("Invalid values provided \n"); + return -1; + } + + val.s = header->valuestring; + val.len = strlen(val.s); + + if (!val.len) { + LM_ERR("No header content provided \n"); + return -1; + } + + if (page->len + val.len + 1 >= max_len) { + LM_ERR("No more HTTP buffer space, have %d, need extra %d into max %d\n",page->len,val.len + 1,max_len); + return -1; + } + + memcpy(page->s + page->len,val.s,val.len); + page->len += val.len; + + memcpy(page->s + page->len, "\n", 1); + page->len += 1; + + value = values->child; + while (value) { + if (value->type != cJSON_Object) { + LM_ERR("Expected value object and found type %d\n",value->type); + goto next_value; + } + + name = cJSON_GetObjectItem(value, "name"); + if (!name || name->type != cJSON_String) { + LM_ERR("Stat name not found \n"); + goto next_value; + } + + counter = cJSON_GetObjectItem(value, "value"); + if (!counter || counter->type != cJSON_Number) { + LM_ERR("Stat name not found \n"); + goto next_value; + } + + val.s = name->valuestring; + val.len = strlen(val.s); + + c = counter->valueint; + cval.s = int2str(c,&cval.len); + + if (page->len + val.len + cval.len + 2 /* blank & \n */ >= max_len) { + LM_ERR("No more HTTP buffer space, have %d, need extra %d into max %d\n",page->len,val.len + cval.len + 2,max_len); + return -1; + } + + memcpy(page->s + page->len,val.s,val.len); + page->len += val.len; + + memcpy(page->s + page->len, " ", 1); + page->len += 1; + + memcpy(page->s + page->len,cval.s,cval.len); + page->len += cval.len; + + memcpy(page->s + page->len, "\n", 1); + page->len += 1; +next_value: + value = value->next; + } + + return 0; +} + +int process_extra_prometheus(char *extra, int len,str *page, int max_len) +{ + cJSON *j_obj=NULL,*arr; + + if (!extra || len <= 0) { + return -1; + } + + j_obj = cJSON_Parse(extra); + if (j_obj == NULL) { + LM_ERR("Failed to parse JSON obj \n"); + return -1; + } + + if (j_obj->type != cJSON_Array) { + LM_ERR("Main JSON object expecting an array \n"); + goto error; + } + + arr = j_obj->child; + while (arr) { + if (process_extra_prometheus_entry(arr,page,max_len) < 0) { + LM_ERR("Failed to process JSON entry \n"); + goto error; + } + + arr=arr->next; + } +error: + cJSON_Delete(j_obj); + + return 0; +} + + int prom_answer_to_connection (void *cls, void *connection, const char *url, const char *method, const char *version, const char *upload_data, @@ -612,6 +780,8 @@ int prom_answer_to_connection (void *cls, void *connection, module_stats *mod; stat_var *stat; int skip_type; + struct sip_msg *route_msg = NULL; + int_str val; LM_DBG("START *** cls=%p, connection=%p, url=%s, method=%s, " "version=%s, upload_data[%d]=%p, *con_cls=%p\n", @@ -682,6 +852,39 @@ int prom_answer_to_connection (void *cls, void *connection, } } } + + if (ref_script_route_is_valid(prometheus_route_ref)) { + /* get a dummy msg for our route */ + route_msg = get_dummy_sip_msg(); + if (!route_msg) { + LM_ERR("Failed to get dummy msg for prometheus route \n"); + goto final; + } + + /* set request route type */ + set_route_type( REQUEST_ROUTE ); + + /* run given hep route */ + run_top_route( sroutes->request[prometheus_route_ref->idx], route_msg); + + memset(&val, 0, sizeof(int_str)); + if (prometheus_avp_name>=0 && + search_first_avp(prometheus_avp_type, prometheus_avp_name, &val, 0) && + val.s.len > 0) { + if (process_extra_prometheus(val.s.s,val.s.len,page,buffer->len) < 0) { + LM_ERR("Failed to add custom prometheus stats \n"); + } + } + + + /* free possible loaded avps */ + reset_avps(); + + release_dummy_sip_msg(route_msg); + } + +final: + if (page->len + 1 >= buffer->len) { LM_ERR("out of memory for stats\n"); prom_groups_free(&groups, &label_groups);