diff --git a/res/prometheus/pjsip_outbound_registrations.c b/res/prometheus/pjsip_outbound_registrations.c new file mode 100644 index 0000000000000000000000000000000000000000..add3648cdf7dc043d1dc6d25565a3ed074c35a52 --- /dev/null +++ b/res/prometheus/pjsip_outbound_registrations.c @@ -0,0 +1,375 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2019 Sangoma, Inc. + * + * Matt Jordan <mjordan@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! + * \file + * \brief Prometheus PJSIP Outbound Registration Metrics + * + * \author Matt Jordan <mjordan@digium.com> + * + */ + +#include "asterisk.h" + +#include "asterisk/stasis_message_router.h" +#include "asterisk/stasis_system.h" +#include "asterisk/res_prometheus.h" + +#ifdef HAVE_PJPROJECT +#include "asterisk/res_pjsip.h" +#endif /* HAVE_PJPROJECT */ + +#include "prometheus_internal.h" + +#ifdef HAVE_PJPROJECT + +/*! \internal \brief Our one and only Stasis message router */ +static struct stasis_message_router *router; + +/*! + * \internal + * \brief Wrapper object around our Metrics + * + * \details We keep a wrapper around the metric so we can easily + * update the value when the state of the registration changes, as + * well as remove and unregsiter the metric when someone destroys + * or reloads the registration + */ +struct prometheus_metric_wrapper { + /*! + * \brief The actual metric. Worth noting that we do *NOT* + * own the metric, as it is registered with res_prometheus. + * Luckily, that module doesn't destroy metrics unless we + * tell it to or if the module unloads. + */ + struct prometheus_metric *metric; + /*! + * \brief Unique key to look up the metric + */ + char key[128]; +}; + +AST_MUTEX_DEFINE_STATIC(metrics_lock); + +/*! + * \internal Vector of metric wrappers + * + * \details + * Why a vector and not an ao2_container? Two reasons: + * (1) There's rarely a ton of outbound registrations, so an ao2_container + * is overkill when we can just walk a vector + * (2) The lifetime of wrappers is well contained + */ +static AST_VECTOR(, struct prometheus_metric_wrapper *) metrics; + +/*! + * \internal + * \brief Create a wrapper for a metric given a key + * + * \param key The unique key + * + * \retval NULL on error + * \retval malloc'd metric wrapper on success + */ +static struct prometheus_metric_wrapper *create_wrapper(const char *key) +{ + struct prometheus_metric_wrapper *wrapper; + + wrapper = ast_calloc(1, sizeof(*wrapper)); + if (!wrapper) { + return NULL; + } + + ast_copy_string(wrapper->key, key, sizeof(wrapper->key)); + return wrapper; +} + +/*! + * \internal + * \brief Get a wrapper by its key + * + * \param key The unqiue key for the wrapper + * + * \retval NULL on no wrapper found :-\ + * \retval wrapper on success + */ +static struct prometheus_metric_wrapper *get_wrapper(const char *key) +{ + int i; + SCOPED_MUTEX(lock, &metrics_lock); + + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric_wrapper *wrapper = AST_VECTOR_GET(&metrics, i); + + if (!strcmp(wrapper->key, key)) { + return wrapper; + } + } + + return NULL; +} + +/*! + * \internal + * \brief Convert an outbound registration state to a numeric value + * + * \param state The state to convert + * + * \retval int representation of the state + */ +static int registration_state_to_int(const char *state) +{ + if (!strcasecmp(state, "Registered")) { + return 1; + } else if (!strcasecmp(state, "Rejected")) { + return 2; + } + return 0; +} + +/*! + * \internal + * \brief Sorcery observer callback called when a registration object is deleted + * + * \param obj The opaque object that was deleted + */ +static void registration_deleted_observer(const void *obj) +{ + struct ast_variable *fields; + struct ast_variable *it_fields; + int i; + SCOPED_MUTEX(lock, &metrics_lock); + + /* + * Because our object is opaque, we have to do some pretty ... interesting + * things here to try and figure out what just happened. + */ + fields = ast_sorcery_objectset_create(ast_sip_get_sorcery(), obj); + if (!fields) { + ast_debug(1, "Unable to convert presumed registry object %p to strings; bailing on delete\n", obj); + return; + } + + for (it_fields = fields; it_fields; it_fields = it_fields->next) { + if (strcasecmp(it_fields->name, "client_uri")) { + continue; + } + + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric_wrapper *wrapper = AST_VECTOR_GET(&metrics, i); + + if (strcmp(wrapper->key, it_fields->value)) { + continue; + } + + ast_debug(1, "Registration metric '%s' deleted; purging with prejudice\n", wrapper->key); + AST_VECTOR_REMOVE(&metrics, i, 1); + /* This will free the metric as well */ + prometheus_metric_unregister(wrapper->metric); + ast_free(wrapper); + } + } + + ast_variables_destroy(fields); +} + +static const struct ast_sorcery_observer registration_observer = { + .deleted = registration_deleted_observer, +}; + +/*! + * \internal + * \brief Sorcery observer called when an object is loaded/reloaded + * + * \param name The name of the object + * \param sorcery The sorcery handle + * \param object_type The type of object + * \param reloaded Whether or not we reloaded the state/definition of the object + * + * \details + * In our case, we only care when we re-load the registration object. We + * wait for the registration to occur in order to create our Prometheus + * metric, so we just punt on object creation. On reload, however, fundamental + * properties of the metric may have been changed, which means we have to remove + * the existing definition of the metric and allow the new registration stasis + * message to re-build it. + */ +static void registration_loaded_observer(const char *name, const struct ast_sorcery *sorcery, const char *object_type, int reloaded) +{ + SCOPED_MUTEX(lock, &metrics_lock); + int i; + + if (!reloaded) { + /* Meh */ + return; + } + + if (strcmp(object_type, "registration")) { + /* Not interested */ + return; + } + + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric_wrapper *wrapper = AST_VECTOR_GET(&metrics, i); + struct ast_variable search_fields = { + .name = "client_uri", + .value = wrapper->key, + .next = NULL, + }; + void *obj; + + ast_debug(1, "Checking for the existance of registration metric %s\n", wrapper->key); + obj = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), object_type, AST_RETRIEVE_FLAG_DEFAULT, &search_fields); + if (!obj) { + ast_debug(1, "Registration metric '%s' not found; purging with prejudice\n", wrapper->key); + AST_VECTOR_REMOVE(&metrics, i, 1); + /* This will free the metric as well */ + prometheus_metric_unregister(wrapper->metric); + ast_free(wrapper); + continue; + } + ao2_ref(obj, -1); + } + +} + +static const struct ast_sorcery_instance_observer observer_callbacks_registrations = { + .object_type_loaded = registration_loaded_observer, +}; + +/*! + * \internal + * \brief Callback for Stasis Registry messages + * + * \param data Callback data, always NULL + * \param sub Stasis subscription + * \param message Our Registry message + * + * \details + * The Stasis Registry message both updates the state of the Prometheus metric + * as well as forces its creation. + */ +static void registry_message_cb(void *data, struct stasis_subscription *sub, + struct stasis_message *message) +{ + struct ast_json_payload *payload = stasis_message_data(message); + struct ast_json *json = payload->json; + const char *username = ast_json_string_get(ast_json_object_get(json, "username")); + const char *status_str = ast_json_string_get(ast_json_object_get(json, "status")); + const char *domain = ast_json_string_get(ast_json_object_get(json, "domain")); + const char *channel_type = ast_json_string_get(ast_json_object_get(json, "channeltype")); + struct prometheus_metric metric = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_GAUGE, + "asterisk_pjsip_outbound_registration_status", + "Current registration status. 0=Unregistered; 1=Registered; 2=Rejected.", + NULL + ); + struct prometheus_metric_wrapper *wrapper; + char eid_str[32]; + + ast_eid_to_str(eid_str, sizeof(eid_str), &ast_eid_default); + + PROMETHEUS_METRIC_SET_LABEL(&metric, 0, "eid", eid_str); + PROMETHEUS_METRIC_SET_LABEL(&metric, 1, "username", username); + PROMETHEUS_METRIC_SET_LABEL(&metric, 2, "domain", domain); + PROMETHEUS_METRIC_SET_LABEL(&metric, 3, "channel_type", channel_type); + snprintf(metric.value, sizeof(metric.value), "%d", registration_state_to_int(status_str)); + + wrapper = get_wrapper(username); + if (wrapper) { + ast_mutex_lock(&wrapper->metric->lock); + /* Safe */ + strcpy(wrapper->metric->value, metric.value); + ast_mutex_unlock(&wrapper->metric->lock); + } else { + wrapper = create_wrapper(username); + if (!wrapper) { + return; + } + + wrapper->metric = prometheus_gauge_create(metric.name, metric.help); + if (!wrapper->metric) { + ast_free(wrapper); + return; + } + *(wrapper->metric) = metric; + + prometheus_metric_register(wrapper->metric); + AST_VECTOR_APPEND(&metrics, wrapper); + } +} + +#endif /* HAVE_PJPROJECT */ + +/*! + * \internal + * \brief Callback invoked when the core module is unloaded + */ +static void pjsip_outbound_registration_metrics_unload_cb(void) +{ +#ifdef HAVE_PJPROJECT + stasis_message_router_unsubscribe_and_join(router); + router = NULL; + ast_sorcery_instance_observer_remove(ast_sip_get_sorcery(), &observer_callbacks_registrations); + ast_sorcery_observer_remove(ast_sip_get_sorcery(), "registration", ®istration_observer); +#endif /* HAVE_PJPROJECT */ +} + +/*! + * \internal + * \brief Metrics provider definition + */ +static struct prometheus_metrics_provider provider = { + .name = "pjsip_outbound_registration", + .unload_cb = pjsip_outbound_registration_metrics_unload_cb, +}; + +int pjsip_outbound_registration_metrics_init(void) +{ + prometheus_metrics_provider_register(&provider); + +#ifdef HAVE_PJPROJECT + router = stasis_message_router_create(ast_system_topic()); + if (!router) { + goto cleanup; + } + + if (stasis_message_router_add(router, ast_system_registry_type(), registry_message_cb, NULL)) { + goto cleanup; + } + + if (ast_sorcery_instance_observer_add(ast_sip_get_sorcery(), &observer_callbacks_registrations)) { + goto cleanup; + } + + if (ast_sorcery_observer_add(ast_sip_get_sorcery(), "registration", ®istration_observer)) { + goto cleanup; + } +#endif /* HAVE_PJPROJECT */ + return 0; + +#ifdef HAVE_PJPROJECT +cleanup: + ao2_cleanup(router); + router = NULL; + ast_sorcery_instance_observer_remove(ast_sip_get_sorcery(), &observer_callbacks_registrations); + ast_sorcery_observer_remove(ast_sip_get_sorcery(), "registration", ®istration_observer); + + return -1; +#endif /* HAVE_PJPROJECT */ +} diff --git a/res/prometheus/prometheus_internal.h b/res/prometheus/prometheus_internal.h index 3b3618aa4fd8bc979612446216153913d028876d..ad6c5282c2e0977dfac88ed90c3c9aea8d870a4b 100644 --- a/res/prometheus/prometheus_internal.h +++ b/res/prometheus/prometheus_internal.h @@ -91,4 +91,12 @@ int endpoint_metrics_init(void); */ int bridge_metrics_init(void); +/*! + * \brief Initialize PJSIP outbound registration metrics + * + * \retval 0 success + * \retval -1 error + */ +int pjsip_outbound_registration_metrics_init(void); + #endif /* #define PROMETHEUS_INTERNAL_H__ */ diff --git a/res/res_prometheus.c b/res/res_prometheus.c index 5181af7823eb507fc0baaea8047fdf5f01fb31e2..fdc90cffe93a30c3487d6e66bdd8d03f9f614a46 100644 --- a/res/res_prometheus.c +++ b/res/res_prometheus.c @@ -25,6 +25,7 @@ */ /*** MODULEINFO + <use>pjproject</use> <support_level>extended</support_level> ***/ @@ -972,7 +973,8 @@ static int load_module(void) if (cli_init() || channel_metrics_init() || endpoint_metrics_init() - || bridge_metrics_init()) { + || bridge_metrics_init() + || pjsip_outbound_registration_metrics_init()) { goto cleanup; }