/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 2019 Sangoma, Inc. * * Matt Jordan * * 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 * */ #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 unregister 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]; }; /*! * \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; AST_MUTEX_DEFINE_STATIC(metrics_lock); /*! * \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 */ }