diff --git a/configs/samples/prometheus.conf.sample b/configs/samples/prometheus.conf.sample new file mode 100644 index 0000000000..63e9bd6d42 --- /dev/null +++ b/configs/samples/prometheus.conf.sample @@ -0,0 +1,61 @@ +; +; res_prometheus Module configuration for Asterisk +; + +; +; Note that this configuration file is consumed by res_prometheus, which +; provides core functionality for serving up Asterisk statistics to a +; Prometheus server. By default, this only includes basic information about +; the Asterisk instance that is running. Additional modules can be loaded to +; provide specific statistics. In all cases, configuration of said statistics +; is done through this configuration file. +; +; Because Prometheus scrapes statistics from HTTP servers, this module requires +; Asterisk's built-in HTTP server to be enabled and configured properly. +; + +; Settings that affect all statistic generation +[general] +enabled = no ; Enable/disable all statistic generation. + ; Default is "no", as enabling this without + ; proper securing of your Asterisk system + ; may result in external systems learning + ; a lot about your Asterisk system. + ; Note #1: If Asterisk's HTTP server is + ; disabled, this setting won't matter. + ; Note #2: It is highly recommended that you + ; set up Basic Auth and configure your + ; Prometheus server to authenticate with + ; Asterisk. Failing to do so will make it easy + ; for external systems to scrape your Asterisk + ; instance and learn things about your system + ; that you may not want them to. While the + ; metrics exposed by this module do not + ; necessarily contain information that can + ; lead to an exploit, an ounce of prevention + ; goes a long way. Particularly for those out + ; there who are exceedingly lax in updating + ; your Asterisk system. You are updating on a + ; regular cadence, aren't you??? +core_metrics_enabled = yes ; Enable/disable core metrics. Core metrics + ; include various properties such as the + ; version of Asterisk, uptime, last reload + ; time, and the overall time it takes to + ; scrape metrics. Default is "yes" +uri = metrics ; The HTTP route to expose metrics on. + ; Default is "metrics". + +; auth_username = Asterisk ; If provided, Basic Auth will be enabled on + ; the metrics route. Failure to provide both + ; auth_username and auth_password will result + ; in a module load error. +; auth_password = ; The password to use for Basic Auth. Note + ; that I'm leaving this blank to prevent + ; you from merely uncommenting the line and + ; running with a config provided password. + ; Because yes, people actually *do* that. + ; I mean, if you're going to do that, just + ; run unsecured. Fake security is usually + ; worse than no security. +; auth_realm = ; Realm to use for authentication. Defaults + ; to Asterisk Prometheus Metrics diff --git a/include/asterisk/res_prometheus.h b/include/asterisk/res_prometheus.h new file mode 100644 index 0000000000..cf62b7b783 --- /dev/null +++ b/include/asterisk/res_prometheus.h @@ -0,0 +1,478 @@ +/* + * res_prometheus: Asterisk Prometheus Metrics + * + * 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. + */ + +#ifndef RES_PROMETHEUS_H__ +#define RES_PROMETHEUS_H__ + +/*! + * \file res_prometheus + * + * \brief Asterisk Prometheus Metrics + * + * This module provides the base APIs and functionality for exposing a + * metrics route in Asterisk's HTTP server suitable for consumption by + * a Prometheus server. It does not provide any metrics itself. + */ + +#include "asterisk/lock.h" +#include "asterisk/linkedlists.h" +#include "asterisk/stringfields.h" + +/*! + * \brief How many labels a single metric can have + */ +#define PROMETHEUS_MAX_LABELS 8 + +/*! + * \brief How long a label name can be + */ +#define PROMETHEUS_MAX_NAME_LENGTH 64 + +/*! + * \brief How long a label value can be + */ +#define PROMETHEUS_MAX_LABEL_LENGTH 128 + +/*! + * \brief How large of a value we can store + */ +#define PROMETHEUS_MAX_VALUE_LENGTH 32 + +/** + * \brief Prometheus general configuration + * + * \details + * While the config file should generally provide the configuration + * for this module, it is useful for testing purposes to allow the + * configuration to be injected into the module. This struct is + * public to allow this to occur. + * + * \note + * Modifying the configuration outside of testing purposes is not + * encouraged. + */ +struct prometheus_general_config { + /*! \brief Whether or not the module is enabled */ + unsigned int enabled; + /*! \brief Whether or not core metrics are enabled */ + unsigned int core_metrics_enabled; + AST_DECLARE_STRING_FIELDS( + /*! \brief The HTTP URI we register ourselves to */ + AST_STRING_FIELD(uri); + /*! \brief Auth username for Basic Auth */ + AST_STRING_FIELD(auth_username); + /*! \brief Auth password for Basic Auth */ + AST_STRING_FIELD(auth_password); + /*! \brief Auth realm */ + AST_STRING_FIELD(auth_realm); + ); +}; + +/*! + * \brief Prometheus metric type + * + * \note + * Clearly, at some point, we should support summaries and histograms. + * As an initial implementation, counters / gauges give us quite a + * bit of functionality. + */ +enum prometheus_metric_type { + /*! + * \brief A metric whose value always goes up + */ + PROMETHEUS_METRIC_COUNTER = 0, + /* + * \brief A metric whose value can bounce around like a jackrabbit + */ + PROMETHEUS_METRIC_GAUGE, +}; + +/*! + * \brief How the metric was allocated. + * + * \note Clearly, you don't want to get this wrong. + */ +enum prometheus_metric_allocation_strategy { + /*! + * \brief The metric was allocated on the stack + */ + PROMETHEUS_METRIC_ALLOCD = 0, + /*! + * \brief The metric was allocated on the heap + */ + PROMETHEUS_METRIC_MALLOCD, +}; + +/*! + * \brief A label that further defines a metric + */ +struct prometheus_label { + /*! + * \brief The name of the label + */ + char name[PROMETHEUS_MAX_NAME_LENGTH]; + /*! + * \brief The value of the label + */ + char value[PROMETHEUS_MAX_LABEL_LENGTH]; +}; + +/*! + * \brief An actual, honest to god, metric. + * + * \details + * A bit of effort has gone into making this structure as efficient as we + * possibly can. Given that a *lot* of metrics can theoretically be dumped out, + * and that Asterisk attempts to be a "real-time" system, we want this process + * to be as efficient as possible. Countering that is the ridiculous flexibility + * that Prometheus allows for (and, to an extent, wants) - namely the notion of + * families of metrics delineated by their labels. + * + * In order to balance this, metrics have arrays of labels. While this makes for + * a very large struct (such that loading one of these into memory is probably + * going to blow your cache), you will at least get the whole thing, since + * you're going to need those labels to figure out what you're looking like. + * + * A hierarchy of metrics occurs when all metrics have the same \c name, but + * different labels. + * + * We manage the hierarchy by allowing a metric to maintain their own list of + * related metrics. When metrics are registered (/c prometheus_metric_register), + * the function will automatically determine the hierarchy and place them into + * the appropriate lists. When you are creating metrics on the fly in a callback + * (\c prometheus_callback_register), you have to manage this hierarchy + * yourself, and only print out the first metric in a chain. + * + * Note that **EVERYTHING** in a metric is immutable once registered, save for + * its value. Modifying the hierarchy, labels, name, help, whatever is going to + * result in a "bad time", and is also expressly against Prometheus law. (Don't + * get your liver eaten.) + */ +struct prometheus_metric { + /*! + * \brief What type of metric we are + */ + enum prometheus_metric_type type; + /*! + * \brief How this metric was allocated + */ + enum prometheus_metric_allocation_strategy allocation_strategy; + /*! + * \brief A lock protecting the metric \c value + * + * \note The metric must be locked prior to updating its value! + */ + ast_mutex_t lock; + /*! + * \brief Pointer to a static string defining this metric's help text. + */ + const char *help; + /*! + * \brief Our metric name + */ + char name[PROMETHEUS_MAX_NAME_LENGTH]; + /*! + * \brief The metric's labels + */ + struct prometheus_label labels[PROMETHEUS_MAX_LABELS]; + /*! + * \brief The current value. + * + * \details + * If \c get_metric_value is set, this value is ignored until the callback + * happens + */ + char value[PROMETHEUS_MAX_VALUE_LENGTH]; + /* + * \brief Callback function to obtain the metric value + * \details + * If updates need to happen when the metric is gathered, provide the + * callback function. Otherwise, leave it \c NULL. + */ + void (* get_metric_value)(struct prometheus_metric *metric); + /*! + * \brief A list of children metrics + * \details + * Children metrics have the same name but different label. + * + * Registration of a metric will automatically nest the metrics; otherwise + * they are treated independently. + * + * The help of the first metric in a chain of related metrics is the only + * one that will be printed. + * + * For metrics output during a callback, the handler is responsible for + * managing the children. For metrics that are registered, the registration + * automatically nests the metrics. + */ + AST_LIST_HEAD_NOLOCK(, prometheus_metric) children; + AST_LIST_ENTRY(prometheus_metric) entry; +}; + +/** + * \brief Convenience macro for initializing a metric on the stack + * + * \param mtype The metric type. See \c prometheus_metric_type + * \param n Name of the metric + * \param h Help text for the metric + * \param cb Callback function. Optional; may be \c NULL + * + * \details + * When initializing a metric on the stack, various fields have to be provided + * to initialize the metric correctly. This macro can be used to simplify the + * process. + * + * Example Usage: + * \code + * struct prometheus_metric test_counter_one = + * PROMETHEUS_METRIC_STATIC_INITIALIZATION( + * PROMETHEUS_METRIC_COUNTER, + * "test_counter_one", + * "A test counter", + * NULL); + * struct prometheus_metric test_counter_two = + * PROMETHEUS_METRIC_STATIC_INITIALIZATION( + * PROMETHEUS_METRIC_COUNTER, + * "test_counter_two", + * "A test counter", + * metric_values_get_counter_value_cb); + * \endcode + * + */ +#define PROMETHEUS_METRIC_STATIC_INITIALIZATION(mtype, n, h, cb) { \ + .type = (mtype), \ + .allocation_strategy = PROMETHEUS_METRIC_ALLOCD, \ + .lock = AST_MUTEX_INIT_VALUE, \ + .name = (n), \ + .help = (h), \ + .children = AST_LIST_HEAD_NOLOCK_INIT_VALUE, \ + .get_metric_value = (cb), \ +} + +/** + * \brief Convenience macro for setting a label / value in a metric + * + * \param metric The metric to set the label on + * \param label Position of the label to set + * \param n Name of the label + * \param v Value of the label + * + * \details + * When creating nested metrics, it's helpful to set their label after they have + * been declared but before they have been registered. This macro acts as a + * convenience function to set the labels properly on a declared metric. + * + * \note Setting labels *after* registration will lead to a "bad time" + * + * Example Usage: + * \code + * PROMETHEUS_METRIC_SET_LABEL( + * test_gauge_child_two, 0, "key_one", "value_two"); + * PROMETHEUS_METRIC_SET_LABEL( + * test_gauge_child_two, 1, "key_two", "value_two"); + * \endcode + * + */ +#define PROMETHEUS_METRIC_SET_LABEL(metric, label, n, v) do { \ + ast_assert((label) < PROMETHEUS_MAX_LABELS); \ + ast_copy_string((metric)->labels[(label)].name, (n), sizeof((metric)->labels[(label)].name)); \ + ast_copy_string((metric)->labels[(label)].value, (v), sizeof((metric)->labels[(label)].value)); \ +} while (0) + +/*! + * \brief Destroy a metric and all its children + * + * \note If you still want the children, make sure you remove the head of the + * \c children list first. + * + * \param metric The metric to destroy + */ +void prometheus_metric_free(struct prometheus_metric *metric); + +/*! + * \brief Create a malloc'd counter metric + * + * \note The metric must be registered after creation + * + * \param name The name of the metric + * \param help Help text for the metric + * + * \retval prometheus_metric on success + * \retval NULL on error + */ +struct prometheus_metric *prometheus_counter_create(const char *name, + const char *help); + +/*! + * \brief Create a malloc'd gauge metric + * + * \note The metric must be registered after creation + * + * \param name The name of the metric + * \param help Help text for the metric + * + * \retval prometheus_metric on success + * \retval NULL on error + */ +struct prometheus_metric *prometheus_gauge_create(const char *name, + const char *help); + +/** + * \brief Convert a metric (and its children) into Prometheus compatible text + * + * \param metric The metric to convert to a string + * \param [out] output The \c ast_str string to populate with the metric(s) + */ +void prometheus_metric_to_string(struct prometheus_metric *metric, + struct ast_str **output); + +/*! + * \brief Defines a callback that will be invoked when the HTTP route is called + * + * \details + * This callback presents the second way of passing metrics to a Prometheus + * server. For metrics that are generated often or whose value needs to be + * stored, metrics can be created and registered. For metrics that can be + * obtained "on-the-fly", this mechanism is preferred. When the HTTP route is + * queried by promtheus, the registered callbacks are invoked. The string passed + * to the callback should be populated with stack-allocated metrics using + * \c prometheus_metric_to_string. + * + * Example Usage: + * \code + * static void prometheus_metric_callback(struct ast_str **output) + * { + * struct prometheus_metric test_counter = + * PROMETHEUS_METRIC_STATIC_INITIALIZATION( + * PROMETHEUS_METRIC_COUNTER, + * "test_counter", + * "A test counter", + * NULL); + * + * prometheus_metric_to_string(&test_counter, output); + * } + * + * static void load_module(void) + * { + * struct prometheus_callback callback = { + * .name = "test_callback", + * .callback_fn = &prometheus_metric_callback, + * }; + * + * prometheus_callback_register(&callback); + * } + * + * \endcode + * + */ +struct prometheus_callback { + /*! + * \brief The name of our callback (always useful for debugging) + */ + const char *name; + /*! + * \brief The callback function to invoke + */ + void (* callback_fn)(struct ast_str **output); +}; + +/*! + * Register a metric for collection + * + * \param metric The metric to register + * + * \retval 0 success + * \retval -1 error + */ +int prometheus_metric_register(struct prometheus_metric *metric); + +/*! + * \brief Remove a registered metric + * + * \param metric The metric to unregister + * + * \note Unregistering also destroys the metric, if found + * + * \retval 0 The metric was found, unregistered, and disposed of + * \retval -1 The metric was not found + */ +int prometheus_metric_unregister(struct prometheus_metric *metric); + +/*! + * The current number of registered metrics + * + * \retval The current number of registered metrics + */ +int prometheus_metric_registered_count(void); + +/*! + * Register a metric callback + * + * \param callback The callback to register + * + * \retval 0 success + * \retval -1 error + */ +int prometheus_callback_register(struct prometheus_callback *callback); + +/*! + * \brief Remove a registered callback + * + * \param callback The callback to unregister + */ +void prometheus_callback_unregister(struct prometheus_callback *callback); + +/*! + * \brief Retrieve the current configuration of the module + * + * \note + * This should primarily be done for testing purposes. + * + * \details + * config is an AO2 ref counted object + * + * \retval NULL on error + * \retval config on success + */ +struct prometheus_general_config *prometheus_general_config_get(void); + +/*! + * \brief Set the configuration for the module + * + * \note + * This should primarily be done for testing purposes + * + * \details + * This is not a ref-stealing function. The reference count to \c config + * will be incremented as a result of calling this method. + * + */ +void prometheus_general_config_set(struct prometheus_general_config *config); + +/*! + * \brief Allocate a new configuration object + * + * \details + * The returned object is an AO2 ref counted object + * + * \retval NULL on error + * \retval config on success + */ +void *prometheus_general_config_alloc(void); + +#endif /* #ifndef RES_PROMETHEUS_H__ */ diff --git a/res/res_prometheus.c b/res/res_prometheus.c new file mode 100644 index 0000000000..1f4e635252 --- /dev/null +++ b/res/res_prometheus.c @@ -0,0 +1,899 @@ +/* + * 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 Core Prometheus metrics API + * + * \author Matt Jordan + * + */ + +/*** MODULEINFO + extended + ***/ + +/*** DOCUMENTATION + + Resource for integration with Prometheus + + + General settings. + + + The general settings section contains information + to configure Asterisk to serve up statistics for a Prometheus server. + + + You must enable Asterisk's HTTP server in http.conf + for this module to function properly! + + + + + Enable or disable Prometheus statistics. + + + + + + + + + Enable or disable core metrics. + + + Core metrics show various properties of the Asterisk system, including + how the binary was built, the version, uptime, last reload time, etc. + Generally, these options are harmless and should always be enabled. + This option mostly exists to disable output of all options for testing + purposes, as well as for those foolish souls who really don't care + what version of Asterisk they're running. + + + + + + + + + The HTTP URI to serve metrics up on. + + + Username to use for Basic Auth. + + + If set, use Basic Auth to authenticate requests to the route + specified by uri. Note that you + will need to configure your Prometheus server with the + appropriate auth credentials. + + + If set, auth_password must also + be set appropriately. + + + + It is highly recommended to set up Basic Auth. Failure + to do so may result in useful information about your + Asterisk system being made easily scrapable by the + wide world. Consider yourself duly warned. + + + + + + Password to use for Basic Auth. + + + If set, this is used in conjunction with auth_username + to require Basic Auth for all requests to the Prometheus metrics. Note that + setting this without auth_username will not + do anything. + + + + + Auth realm used in challenge responses + + + + +***/ + +#define AST_MODULE_SELF_SYM __internal_res_prometheus_self + +#include "asterisk.h" + +#include "asterisk/module.h" +#include "asterisk/vector.h" +#include "asterisk/http.h" +#include "asterisk/config_options.h" +#include "asterisk/ast_version.h" +#include "asterisk/buildinfo.h" +#include "asterisk/res_prometheus.h" + +/*! \brief Lock that protects data structures during an HTTP scrape */ +AST_MUTEX_DEFINE_STATIC(scrape_lock); + +AST_VECTOR(, struct prometheus_metric *) metrics; + +AST_VECTOR(, struct prometheus_callback *) callbacks; + +/*! \brief The actual module config */ +struct module_config { + /*! \brief General settings */ + struct prometheus_general_config *general; +}; + +static struct aco_type global_option = { + .type = ACO_GLOBAL, + .name = "general", + .item_offset = offsetof(struct module_config, general), + .category_match = ACO_WHITELIST_EXACT, + .category = "general", +}; + +struct aco_type *global_options[] = ACO_TYPES(&global_option); + +struct aco_file prometheus_conf = { + .filename = "prometheus.conf", + .types = ACO_TYPES(&global_option), +}; + +/*! \brief The module configuration container */ +static AO2_GLOBAL_OBJ_STATIC(global_config); + +static void *module_config_alloc(void); +static int prometheus_config_pre_apply(void); +static void prometheus_config_post_apply(void); +/*! \brief Register information about the configs being processed by this module */ +CONFIG_INFO_STANDARD(cfg_info, global_config, module_config_alloc, + .files = ACO_FILES(&prometheus_conf), + .pre_apply_config = prometheus_config_pre_apply, + .post_apply_config = prometheus_config_post_apply, +); + +#define CORE_PROPERTIES_HELP "Asterisk instance properties. The value of this will always be 1." + +#define CORE_UPTIME_HELP "Asterisk instance uptime in seconds." + +#define CORE_LAST_RELOAD_HELP "Time since last Asterisk reload in seconds." + +#define CORE_METRICS_SCRAPE_TIME_HELP "Total time taken to collect metrics, in milliseconds" + +static void get_core_uptime_cb(struct prometheus_metric *metric) +{ + struct timeval now = ast_tvnow(); + int64_t duration = ast_tvdiff_sec(now, ast_startuptime); + + snprintf(metric->value, sizeof(metric->value), "%" PRIu64, duration); +} + +static void get_last_reload_cb(struct prometheus_metric *metric) +{ + struct timeval now = ast_tvnow(); + int64_t duration = ast_tvdiff_sec(now, ast_lastreloadtime); + + snprintf(metric->value, sizeof(metric->value), "%" PRIu64, duration); +} + +/*! + * \brief The scrape duration metric + * + * \details + * This metric is special in that it should never be registered. + * Instead, the HTTP callback function that walks the metrics will + * always populate this metric explicitly if core metrics + * are enabled. + */ +static struct prometheus_metric core_scrape_metric = + PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "asterisk_core_scrape_time_ms", + CORE_METRICS_SCRAPE_TIME_HELP, + NULL); + +#define METRIC_CORE_PROPS_ARRAY_INDEX 0 +/*! + * \brief Core metrics to scrape + */ +static struct prometheus_metric core_metrics[] = { + PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "asterisk_core_properties", + CORE_PROPERTIES_HELP, + NULL), + PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "asterisk_core_uptime_seconds", + CORE_UPTIME_HELP, + get_core_uptime_cb), + PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "asterisk_core_last_reload_seconds", + CORE_LAST_RELOAD_HELP, + get_last_reload_cb), +}; + +/** + * \internal + * \brief Compare two metrics to see if their name / labels / values match + * + * \param left The first metric to compare + * \param right The second metric to compare + * + * \retval 0 The metrics are not the same + * \retval 1 The metrics are the same + */ +static int prometheus_metric_cmp(struct prometheus_metric *left, + struct prometheus_metric *right) +{ + int i; + ast_debug(5, "Comparison: Names %s == %s\n", left->name, right->name); + if (strcmp(left->name, right->name)) { + return 0; + } + + for (i = 0; i < PROMETHEUS_MAX_LABELS; i++) { + ast_debug(5, "Comparison: Label %d Names %s == %s\n", i, + left->labels[i].name, right->labels[i].name); + if (strcmp(left->labels[i].name, right->labels[i].name)) { + return 0; + } + + ast_debug(5, "Comparison: Label %d Values %s == %s\n", i, + left->labels[i].value, right->labels[i].value); + if (strcmp(left->labels[i].value, right->labels[i].value)) { + return 0; + } + } + + ast_debug(5, "Copmarison: %s (%p) is equal to %s (%p)\n", + left->name, left, right->name, right); + return 1; +} + +int prometheus_metric_registered_count(void) +{ + SCOPED_MUTEX(lock, &scrape_lock); + + return AST_VECTOR_SIZE(&metrics); +} + +int prometheus_metric_register(struct prometheus_metric *metric) +{ + SCOPED_MUTEX(lock, &scrape_lock); + int i; + + if (!metric) { + return -1; + } + + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric *existing = AST_VECTOR_GET(&metrics, i); + struct prometheus_metric *child; + + if (prometheus_metric_cmp(existing, metric)) { + ast_log(AST_LOG_NOTICE, + "Refusing registration of existing Prometheus metric: %s\n", + metric->name); + return -1; + } + + AST_LIST_TRAVERSE(&existing->children, child, entry) { + if (prometheus_metric_cmp(child, metric)) { + ast_log(AST_LOG_NOTICE, + "Refusing registration of existing Prometheus metric: %s\n", + metric->name); + return -1; + } + } + + if (!strcmp(metric->name, existing->name)) { + ast_debug(3, "Nesting metric '%s' as child (%p) under existing (%p)\n", + metric->name, metric, existing); + AST_LIST_INSERT_TAIL(&existing->children, metric, entry); + return 0; + } + } + + ast_debug(3, "Tracking new root metric '%s'\n", metric->name); + if (AST_VECTOR_APPEND(&metrics, metric)) { + ast_log(AST_LOG_WARNING, "Failed to grow vector to make room for Prometheus metric: %s\n", + metric->name); + return -1; + } + + return 0; +} + +int prometheus_metric_unregister(struct prometheus_metric *metric) +{ + if (!metric) { + return -1; + } + + { + SCOPED_MUTEX(lock, &scrape_lock); + int i; + + ast_debug(3, "Removing metric '%s'\n", metric->name); + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric *existing = AST_VECTOR_GET(&metrics, i); + + /* + * If this is a complete match, remove the matching metric + * and place its children back into the list + */ + if (prometheus_metric_cmp(existing, metric)) { + struct prometheus_metric *root; + + AST_VECTOR_REMOVE(&metrics, i, 1); + root = AST_LIST_REMOVE_HEAD(&existing->children, entry); + if (root) { + struct prometheus_metric *child; + AST_LIST_TRAVERSE_SAFE_BEGIN(&existing->children, child, entry) { + AST_LIST_REMOVE_CURRENT(entry); + AST_LIST_INSERT_TAIL(&root->children, child, entry); + } + AST_LIST_TRAVERSE_SAFE_END; + AST_VECTOR_INSERT_AT(&metrics, i, root); + } + prometheus_metric_free(existing); + return 0; + } + + /* + * Name match, but labels don't match. Find the matching entry with + * labels and remove it along with all of its children + */ + if (!strcmp(existing->name, metric->name)) { + struct prometheus_metric *child; + + AST_LIST_TRAVERSE_SAFE_BEGIN(&existing->children, child, entry) { + if (prometheus_metric_cmp(child, metric)) { + AST_LIST_REMOVE_CURRENT(entry); + prometheus_metric_free(child); + return 0; + } + } + AST_LIST_TRAVERSE_SAFE_END; + } + } + } + + return -1; +} + +void prometheus_metric_free(struct prometheus_metric *metric) +{ + struct prometheus_metric *child; + + if (!metric) { + return; + } + + while ((child = AST_LIST_REMOVE_HEAD(&metric->children, entry))) { + prometheus_metric_free(child); + } + ast_mutex_destroy(&metric->lock); + + if (metric->allocation_strategy == PROMETHEUS_METRIC_ALLOCD) { + return; + } else if (metric->allocation_strategy == PROMETHEUS_METRIC_MALLOCD) { + ast_free(metric); + } +} + +/** + * \internal + * \brief Common code for creating a metric + * + * \param name The name of the metric + * \param help Help string to output when rendered. This must be static. + * + * \retval \c prometheus_metric on success + * \retval NULL on failure + */ +static struct prometheus_metric *prometheus_metric_create(const char *name, const char *help) +{ + struct prometheus_metric *metric = NULL; + + metric = ast_calloc(1, sizeof(*metric)); + if (!metric) { + return NULL; + } + metric->allocation_strategy = PROMETHEUS_METRIC_MALLOCD; + ast_mutex_init(&metric->lock); + + ast_copy_string(metric->name, name, sizeof(metric->name)); + metric->help = help; + + return metric; +} + +struct prometheus_metric *prometheus_gauge_create(const char *name, const char *help) +{ + struct prometheus_metric *metric; + + metric = prometheus_metric_create(name, help); + if (!metric) { + return NULL; + } + metric->type = PROMETHEUS_METRIC_GAUGE; + + return metric; +} + +struct prometheus_metric *prometheus_counter_create(const char *name, const char *help) +{ + struct prometheus_metric *metric; + + metric = prometheus_metric_create(name, help); + if (!metric) { + return NULL; + } + metric->type = PROMETHEUS_METRIC_COUNTER; + + return metric; +} + +static const char *prometheus_metric_type_to_string(enum prometheus_metric_type type) +{ + switch (type) { + case PROMETHEUS_METRIC_COUNTER: + return "counter"; + case PROMETHEUS_METRIC_GAUGE: + return "gauge"; + default: + ast_assert(0); + return "unknown"; + } +} + +/** + * \internal + * \brief Render a metric to text + * + * \param metric The metric to render + * \param output The string buffer to append the text to + */ +static void prometheus_metric_full_to_string(struct prometheus_metric *metric, + struct ast_str **output) +{ + int i; + int labels_exist = 0; + + ast_str_append(output, 0, "%s", metric->name); + + for (i = 0; i < PROMETHEUS_MAX_LABELS; i++) { + if (!ast_strlen_zero(metric->labels[i].name)) { + labels_exist = 1; + if (i == 0) { + ast_str_append(output, 0, "%s", "{"); + } else { + ast_str_append(output, 0, "%s", ","); + } + ast_str_append(output, 0, "%s=\"%s\"", + metric->labels[i].name, + metric->labels[i].value); + } + } + + if (labels_exist) { + ast_str_append(output, 0, "%s", "}"); + } + + /* + * If no value exists, put in a 0. That ensures we don't anger Prometheus. + */ + if (ast_strlen_zero(metric->value)) { + ast_str_append(output, 0, " 0\n"); + } else { + ast_str_append(output, 0, " %s\n", metric->value); + } +} + +void prometheus_metric_to_string(struct prometheus_metric *metric, + struct ast_str **output) +{ + struct prometheus_metric *child; + + ast_str_append(output, 0, "# HELP %s %s\n", metric->name, metric->help); + ast_str_append(output, 0, "# TYPE %s %s\n", metric->name, + prometheus_metric_type_to_string(metric->type)); + prometheus_metric_full_to_string(metric, output); + AST_LIST_TRAVERSE(&metric->children, child, entry) { + prometheus_metric_full_to_string(child, output); + } +} + +int prometheus_callback_register(struct prometheus_callback *callback) +{ + SCOPED_MUTEX(lock, &scrape_lock); + + if (!callback || !callback->callback_fn || ast_strlen_zero(callback->name)) { + return -1; + } + + AST_VECTOR_APPEND(&callbacks, callback); + + return 0; +} + +void prometheus_callback_unregister(struct prometheus_callback *callback) +{ + SCOPED_MUTEX(lock, &scrape_lock); + int i; + + for (i = 0; i < AST_VECTOR_SIZE(&callbacks); i++) { + struct prometheus_callback *entry = AST_VECTOR_GET(&callbacks, i); + + if (!strcmp(callback->name, entry->name)) { + AST_VECTOR_REMOVE(&callbacks, i, 1); + return; + } + } +} + +static int http_callback(struct ast_tcptls_session_instance *ser, + const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, + struct ast_variable *get_params, struct ast_variable *headers) +{ + RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(global_config), ao2_cleanup); + struct ast_str *response = NULL; + struct timeval start; + struct timeval end; + int i; + + /* If there is no module config or we're not enabled, we can't handle requests */ + if (!mod_cfg || !mod_cfg->general->enabled) { + goto err503; + } + + if (!ast_strlen_zero(mod_cfg->general->auth_username)) { + struct ast_http_auth *http_auth; + + http_auth = ast_http_get_auth(headers); + if (!http_auth) { + goto err401; + } + + if (strcmp(http_auth->userid, mod_cfg->general->auth_username)) { + ast_debug(5, "Invalid username provided for auth request: %s\n", http_auth->userid); + ao2_ref(http_auth, -1); + goto err401; + } + + if (strcmp(http_auth->password, mod_cfg->general->auth_password)) { + ast_debug(5, "Invalid password provided for auth request: %s\n", http_auth->password); + ao2_ref(http_auth, -1); + goto err401; + } + + ao2_ref(http_auth, -1); + } + + response = ast_str_create(512); + if (!response) { + goto err500; + } + + if (mod_cfg->general->core_metrics_enabled) { + start = ast_tvnow(); + } + + ast_mutex_lock(&scrape_lock); + for (i = 0; i < AST_VECTOR_SIZE(&callbacks); i++) { + struct prometheus_callback *callback = AST_VECTOR_GET(&callbacks, i); + + callback->callback_fn(&response); + } + + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric *metric = AST_VECTOR_GET(&metrics, i); + + ast_mutex_lock(&metric->lock); + if (metric->get_metric_value) { + metric->get_metric_value(metric); + } + prometheus_metric_to_string(metric, &response); + ast_mutex_unlock(&metric->lock); + } + + if (mod_cfg->general->core_metrics_enabled) { + int64_t duration; + + end = ast_tvnow(); + duration = ast_tvdiff_ms(end, start); + snprintf(core_scrape_metric.value, + sizeof(core_scrape_metric.value), + "%" PRIu64, + duration); + prometheus_metric_to_string(&core_scrape_metric, &response); + } + ast_mutex_unlock(&scrape_lock); + + ast_http_send(ser, method, 200, "OK", NULL, response, 0, 0); + + return 0; + +err401: + { + struct ast_str *auth_challenge_headers; + + auth_challenge_headers = ast_str_create(128); + if (!auth_challenge_headers) { + goto err500; + } + ast_str_append(&auth_challenge_headers, 0, + "WWW-Authenticate: Basic realm=\"%s\"\r\n", + mod_cfg->general->auth_realm); + /* ast_http_send takes ownership of the ast_str */ + ast_http_send(ser, method, 401, "Unauthorized", auth_challenge_headers, NULL, 0, 1); + } + ast_free(response); + return 0; +err503: + ast_http_send(ser, method, 503, "Service Unavailable", NULL, NULL, 0, 1); + ast_free(response); + return 0; +err500: + ast_http_send(ser, method, 500, "Server Error", NULL, NULL, 0, 1); + ast_free(response); + return 0; +} + +static void prometheus_general_config_dtor(void *obj) +{ + struct prometheus_general_config *config = obj; + + ast_string_field_free_memory(config); +} + +void *prometheus_general_config_alloc(void) +{ + struct prometheus_general_config *config; + + config = ao2_alloc(sizeof(*config), prometheus_general_config_dtor); + if (!config || ast_string_field_init(config, 32)) { + return NULL; + } + + return config; +} + +struct prometheus_general_config *prometheus_general_config_get(void) +{ + RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(global_config), ao2_cleanup); + + if (!mod_cfg) { + return NULL; + } + ao2_bump(mod_cfg->general); + + return mod_cfg->general; +} + +void prometheus_general_config_set(struct prometheus_general_config *config) +{ + RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(global_config), ao2_cleanup); + + if (!mod_cfg) { + return; + } + ao2_replace(mod_cfg->general, config); + prometheus_config_post_apply(); +} + + +/*! \brief Configuration object destructor */ +static void module_config_dtor(void *obj) +{ + struct module_config *config = obj; + + if (config->general) { + ao2_ref(config->general, -1); + } +} + +/*! \brief Module config constructor */ +static void *module_config_alloc(void) +{ + struct module_config *config; + + config = ao2_alloc(sizeof(*config), module_config_dtor); + if (!config) { + return NULL; + } + + config->general = prometheus_general_config_alloc(); + if (!config->general) { + ao2_ref(config, -1); + config = NULL; + } + + return config; +} + +static struct ast_http_uri prometheus_uri = { + .description = "Prometheus Metrics URI", + .callback = http_callback, + .has_subtree = 1, + .data = NULL, + .key = __FILE__, +}; + +/*! + * \brief Pre-apply callback for the config framework. + * + * This validates that required fields exist and are populated. + */ +static int prometheus_config_pre_apply(void) +{ + struct module_config *config = aco_pending_config(&cfg_info); + + if (!config->general->enabled) { + /* If we're not enabled, we don't care about anything else */ + return 0; + } + + if (!ast_strlen_zero(config->general->auth_username) + && ast_strlen_zero(config->general->auth_password)) { + ast_log(AST_LOG_ERROR, "'auth_username' set without a corresponding 'auth_password'\n"); + return -1; + } + + return 0; +} + +/*! + * \brief Post-apply callback for the config framework. + * + * This sets any run-time information derived from the configuration + */ +static void prometheus_config_post_apply(void) +{ + RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(global_config), ao2_cleanup); + int i; + + /* We can get away with this as the lifetime of the URI + * registered with the HTTP core is contained within + * the lifetime of the module configuration + */ + prometheus_uri.uri = mod_cfg->general->uri; + + /* Re-register the core metrics */ + for (i = 0; i < ARRAY_LEN(core_metrics); i++) { + prometheus_metric_unregister(&core_metrics[i]); + } + if (mod_cfg->general->core_metrics_enabled) { + char eid_str[32]; + ast_eid_to_str(eid_str, sizeof(eid_str), &ast_eid_default); + + PROMETHEUS_METRIC_SET_LABEL(&core_scrape_metric, 0, "eid", eid_str); + + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX], + 1, "version", ast_get_version()); + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX], + 2, "build_options", ast_get_build_opts()); + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX], + 3, "build_date", ast_build_date); + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX], + 4, "build_os", ast_build_os); + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX], + 5, "build_kernel", ast_build_kernel); + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX], + 6, "build_host", ast_build_hostname); + snprintf(core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX].value, + sizeof(core_metrics[METRIC_CORE_PROPS_ARRAY_INDEX].value), + "%d", 1); + + for (i = 0; i < ARRAY_LEN(core_metrics); i++) { + PROMETHEUS_METRIC_SET_LABEL(&core_metrics[i], 0, "eid", eid_str); + prometheus_metric_register(&core_metrics[i]); + } + } +} + +static int unload_module(void) +{ + SCOPED_MUTEX(lock, &scrape_lock); + int i; + + ast_http_uri_unlink(&prometheus_uri); + + for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) { + struct prometheus_metric *metric = AST_VECTOR_GET(&metrics, i); + + prometheus_metric_free(metric); + } + AST_VECTOR_FREE(&metrics); + + AST_VECTOR_FREE(&callbacks); + + aco_info_destroy(&cfg_info); + ao2_global_obj_release(global_config); + + return 0; +} + +static int reload_module(void) { + SCOPED_MUTEX(lock, &scrape_lock); + + ast_http_uri_unlink(&prometheus_uri); + if (aco_process_config(&cfg_info, 1) == ACO_PROCESS_ERROR) { + return -1; + } + if (ast_http_uri_link(&prometheus_uri)) { + ast_log(AST_LOG_WARNING, "Failed to re-register Prometheus Metrics URI during reload\n"); + return -1; + } + + return 0; +} + +static int load_module(void) +{ + SCOPED_MUTEX(lock, &scrape_lock); + + if (AST_VECTOR_INIT(&metrics, 64)) { + goto cleanup; + } + + if (AST_VECTOR_INIT(&callbacks, 8)) { + goto cleanup; + } + + if (aco_info_init(&cfg_info)) { + goto cleanup; + } + aco_option_register(&cfg_info, "enabled", ACO_EXACT, global_options, "no", OPT_BOOL_T, 1, FLDSET(struct prometheus_general_config, enabled)); + aco_option_register(&cfg_info, "core_metrics_enabled", ACO_EXACT, global_options, "yes", OPT_BOOL_T, 1, FLDSET(struct prometheus_general_config, core_metrics_enabled)); + aco_option_register(&cfg_info, "uri", ACO_EXACT, global_options, "", OPT_STRINGFIELD_T, 1, STRFLDSET(struct prometheus_general_config, uri)); + aco_option_register(&cfg_info, "auth_username", ACO_EXACT, global_options, "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct prometheus_general_config, auth_username)); + aco_option_register(&cfg_info, "auth_password", ACO_EXACT, global_options, "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct prometheus_general_config, auth_password)); + aco_option_register(&cfg_info, "auth_realm", ACO_EXACT, global_options, "Asterisk Prometheus Metrics", OPT_STRINGFIELD_T, 0, STRFLDSET(struct prometheus_general_config, auth_realm)); + if (aco_process_config(&cfg_info, 0) == ACO_PROCESS_ERROR) { + goto cleanup; + } + + if (ast_http_uri_link(&prometheus_uri)) { + goto cleanup; + } + + return AST_MODULE_LOAD_SUCCESS; + +cleanup: + ast_http_uri_unlink(&prometheus_uri); + aco_info_destroy(&cfg_info); + AST_VECTOR_FREE(&metrics); + AST_VECTOR_FREE(&callbacks); + + return AST_MODULE_LOAD_DECLINE; +} + + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Asterisk Prometheus Module", + .support_level = AST_MODULE_SUPPORT_EXTENDED, + .load = load_module, + .unload = unload_module, + .reload = reload_module, + .load_pri = AST_MODPRI_DEFAULT, +); diff --git a/res/res_prometheus.exports.in b/res/res_prometheus.exports.in new file mode 100644 index 0000000000..cec31c6af3 --- /dev/null +++ b/res/res_prometheus.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXprometheus*; + local: + *; +}; diff --git a/tests/test_res_prometheus.c b/tests/test_res_prometheus.c new file mode 100644 index 0000000000..01279bed72 --- /dev/null +++ b/tests/test_res_prometheus.c @@ -0,0 +1,829 @@ +/* + * 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. + */ + +/*** MODULEINFO + TEST_FRAMEWORK + res_prometheus + curl + extended + ***/ + +#include "asterisk.h" + +#include + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/config.h" +#include "asterisk/res_prometheus.h" + +#define CATEGORY "/res/prometheus/" + +static char server_uri[512]; + +struct prometheus_general_config *module_config; + +static void curl_free_wrapper(void *ptr) +{ + if (!ptr) { + return; + } + + curl_easy_cleanup(ptr); +} + +static void prometheus_metric_free_wrapper(void *ptr) +{ + if (prometheus_metric_unregister(ptr)) { + prometheus_metric_free(ptr); + } +} + +#define GLOBAL_USERAGENT "asterisk-libcurl-agent/1.0" + +static struct prometheus_general_config *config_alloc(void) +{ + struct prometheus_general_config *config; + + config = prometheus_general_config_alloc(); + if (!config) { + return NULL; + } + + /* Set what we need on the config for most tests */ + ast_string_field_set(config, uri, "test_metrics"); + config->enabled = 1; + config->core_metrics_enabled = 0; + + return config; +} + +static CURL *get_curl_instance(void) +{ + CURL *curl; + + curl = curl_easy_init(); + if (!curl) { + return NULL; + } + + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 180); + curl_easy_setopt(curl, CURLOPT_USERAGENT, GLOBAL_USERAGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curl, CURLOPT_URL, server_uri); + + return curl; +} + +static size_t curl_write_string_callback(void *contents, size_t size, size_t nmemb, void *userdata) +{ + struct ast_str **buffer = userdata; + size_t realsize = size * nmemb; + char *rawdata; + + rawdata = ast_malloc(realsize + 1); + if (!rawdata) { + return 0; + } + memcpy(rawdata, contents, realsize); + rawdata[realsize] = 0; + ast_str_append(buffer, 0, "%s", rawdata); + ast_free(rawdata); + + return realsize; +} + +static void metric_values_get_counter_value_cb(struct prometheus_metric *metric) +{ + strcpy(metric->value, "2"); +} + +AST_TEST_DEFINE(metric_values) +{ + RAII_VAR(CURL *, curl, NULL, curl_free_wrapper); + RAII_VAR(struct ast_str *, buffer, NULL, ast_free); + int res; + struct prometheus_metric test_counter_one = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter_one", + "A test counter", + NULL); + struct prometheus_metric test_counter_two = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter_two", + "A test counter", + metric_values_get_counter_value_cb); + enum ast_test_result_state result = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test value generation/respecting in metrics"; + info->description = + "Metrics have two ways to provide values when the HTTP callback\n" + "is invoked:\n" + "1. By using the direct value that resides in the metric\n" + "2. By providing a callback function to specify the value\n" + "This test verifies that both function appropriately when the\n" + "HTTP callback is called."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + buffer = ast_str_create(128); + if (!buffer) { + return AST_TEST_FAIL; + } + + curl = get_curl_instance(); + if (!curl) { + return AST_TEST_FAIL; + } + + ast_test_validate_cleanup(test, prometheus_metric_register(&test_counter_one) == 0, result, metric_values_cleanup); + ast_test_validate_cleanup(test, prometheus_metric_register(&test_counter_two) == 0, result, metric_values_cleanup); + strcpy(test_counter_one.value, "1"); + + ast_test_status_update(test, " -> CURLing request...\n"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_string_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + result = AST_TEST_FAIL; + goto metric_values_cleanup; + } + + ast_test_status_update(test, " -> Retrieved: %s\n", ast_str_buffer(buffer)); + ast_test_validate_cleanup(test, strcmp(ast_str_buffer(buffer), + "# HELP test_counter_one A test counter\n" + "# TYPE test_counter_one counter\n" + "test_counter_one 1\n" + "# HELP test_counter_two A test counter\n" + "# TYPE test_counter_two counter\n" + "test_counter_two 2\n") == 0, result, metric_values_cleanup); + +metric_values_cleanup: + prometheus_metric_unregister(&test_counter_one); + prometheus_metric_unregister(&test_counter_two); + + return result; +} + +static void prometheus_metric_callback(struct ast_str **output) +{ + struct prometheus_metric test_counter = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter", + "A test counter", + NULL); + + prometheus_metric_to_string(&test_counter, output); +} + +AST_TEST_DEFINE(metric_callback_register) +{ + RAII_VAR(CURL *, curl, NULL, curl_free_wrapper); + RAII_VAR(struct ast_str *, buffer, NULL, ast_free); + int res; + struct prometheus_callback callback = { + .name = "test_callback", + .callback_fn = &prometheus_metric_callback, + }; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test registration of callbacks"; + info->description = + "This test covers callback registration. It registers\n" + "a callback that is invoked when an HTTP request is made,\n" + "and it verifies that during said callback the output to\n" + "the response string is correctly appended to. It also verifies\n" + "that unregistered callbacks are not invoked."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + buffer = ast_str_create(128); + if (!buffer) { + return AST_TEST_FAIL; + } + + ast_test_validate(test, prometheus_callback_register(&callback) == 0); + + curl = get_curl_instance(); + if (!curl) { + return AST_TEST_NOT_RUN; + } + + ast_test_status_update(test, " -> CURLing request...\n"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_string_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + return AST_TEST_FAIL; + } + + ast_test_status_update(test, " -> Retrieved: %s\n", ast_str_buffer(buffer)); + ast_test_validate(test, strcmp(ast_str_buffer(buffer), + "# HELP test_counter A test counter\n" + "# TYPE test_counter counter\n" + "test_counter 0\n") == 0); + + prometheus_callback_unregister(&callback); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(metric_register) +{ + struct prometheus_metric test_counter = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter", + "A test counter", + NULL); + RAII_VAR(struct prometheus_metric *, test_gauge, NULL, prometheus_metric_free_wrapper); + RAII_VAR(struct prometheus_metric *, test_gauge_child_one, NULL, prometheus_metric_free_wrapper); + RAII_VAR(struct prometheus_metric *, test_gauge_child_two, NULL, prometheus_metric_free_wrapper); + RAII_VAR(struct prometheus_metric *, bad_metric, NULL, prometheus_metric_free_wrapper); + enum ast_test_result_state result; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test registration of metrics"; + info->description = + "This test covers the following registration scenarios:\n" + "- Nominal registration of simple metrics\n" + "- Registration of metrics with different allocation strategies\n" + "- Nested metrics with label families\n" + "- Off nominal registration with simple name collisions\n" + "- Off nominal registration with label collisions"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_status_update(test, "Testing nominal registration\n"); + ast_test_status_update(test, "-> Static metric\n"); + ast_test_validate_cleanup(test, prometheus_metric_register(&test_counter) == 0, result, metric_register_cleanup); + ast_test_status_update(test, "-> Malloc'd metric\n"); + test_gauge = prometheus_gauge_create("test_gauge", "A test gauge"); + ast_test_validate(test, test_gauge != NULL); + ast_test_validate_cleanup(test, prometheus_metric_register(test_gauge) == 0, result, metric_register_cleanup); + ast_test_validate_cleanup(test, prometheus_metric_registered_count() == 2, result, metric_register_cleanup); + + ast_test_status_update(test, "Testing nominal registration of child metrics\n"); + test_gauge_child_one = prometheus_gauge_create("test_gauge", "A test gauge"); + ast_test_validate_cleanup(test, test_gauge_child_one != NULL, result, metric_register_cleanup); + PROMETHEUS_METRIC_SET_LABEL(test_gauge_child_one, 0, "key_one", "value_one"); + PROMETHEUS_METRIC_SET_LABEL(test_gauge_child_one, 1, "key_two", "value_one"); + test_gauge_child_two = prometheus_gauge_create("test_gauge", "A test gauge"); + ast_test_validate_cleanup(test, test_gauge_child_two != NULL, result, metric_register_cleanup); + PROMETHEUS_METRIC_SET_LABEL(test_gauge_child_two, 0, "key_one", "value_two"); + PROMETHEUS_METRIC_SET_LABEL(test_gauge_child_two, 1, "key_two", "value_two"); + ast_test_validate_cleanup(test, prometheus_metric_register(test_gauge_child_one) == 0, result, metric_register_cleanup); + ast_test_validate_cleanup(test, prometheus_metric_register(test_gauge_child_two) == 0, result, metric_register_cleanup); + ast_test_validate_cleanup(test, prometheus_metric_registered_count() == 2, result, metric_register_cleanup); + ast_test_validate_cleanup(test, test_gauge->children.first == test_gauge_child_one, result, metric_register_cleanup); + ast_test_validate_cleanup(test, test_gauge->children.last == test_gauge_child_two, result, metric_register_cleanup); + + ast_test_status_update(test, "Testing name collisions\n"); + bad_metric = prometheus_counter_create("test_counter", "A test counter"); + ast_test_validate_cleanup(test, bad_metric != NULL, result, metric_register_cleanup); + ast_test_validate_cleanup(test, prometheus_metric_register(bad_metric) != 0, result, metric_register_cleanup); + prometheus_metric_free(bad_metric); + bad_metric = NULL; + + ast_test_status_update(test, "Testing label collisions\n"); + bad_metric = prometheus_gauge_create("test_gauge", "A test gauge"); + ast_test_validate_cleanup(test, bad_metric != NULL, result, metric_register_cleanup); + PROMETHEUS_METRIC_SET_LABEL(bad_metric, 0, "key_one", "value_one"); + PROMETHEUS_METRIC_SET_LABEL(bad_metric, 1, "key_two", "value_one"); + ast_test_validate_cleanup(test, prometheus_metric_register(bad_metric) != 0, result, metric_register_cleanup); + prometheus_metric_free(bad_metric); + bad_metric = NULL; + + ast_test_status_update(test, "Testing removal of metrics\n"); + prometheus_metric_unregister(test_gauge_child_two); + test_gauge_child_two = NULL; + + ast_test_validate_cleanup(test, prometheus_metric_registered_count() == 2, result, metric_register_cleanup); + prometheus_metric_unregister(test_gauge); + test_gauge = NULL; + + ast_test_validate_cleanup(test, prometheus_metric_registered_count() == 2, result, metric_register_cleanup); + prometheus_metric_unregister(test_gauge_child_one); + test_gauge_child_one = NULL; + + ast_test_validate_cleanup(test, prometheus_metric_registered_count() == 1, result, metric_register_cleanup); + prometheus_metric_unregister(&test_counter); + + ast_test_validate_cleanup(test, prometheus_metric_registered_count() == 0, result, metric_register_cleanup); + + return AST_TEST_PASS; + +metric_register_cleanup: + prometheus_metric_unregister(&test_counter); + return result; +} + +AST_TEST_DEFINE(counter_to_string) +{ + struct prometheus_metric test_counter = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter", + "A test counter", + NULL); + struct prometheus_metric test_counter_child_one = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter", + "A test counter", + NULL); + struct prometheus_metric test_counter_child_two = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_COUNTER, + "test_counter", + "A test counter", + NULL); + RAII_VAR(struct ast_str *, buffer, NULL, ast_free); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test formatting of counters"; + info->description = + "This test covers the formatting of printed counters"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + buffer = ast_str_create(128); + if (!buffer) { + return AST_TEST_FAIL; + } + + PROMETHEUS_METRIC_SET_LABEL(&test_counter_child_one, 0, "key_one", "value_one"); + PROMETHEUS_METRIC_SET_LABEL(&test_counter_child_one, 1, "key_two", "value_one"); + PROMETHEUS_METRIC_SET_LABEL(&test_counter_child_two, 0, "key_one", "value_two"); + PROMETHEUS_METRIC_SET_LABEL(&test_counter_child_two, 1, "key_two", "value_two"); + AST_LIST_INSERT_TAIL(&test_counter.children, &test_counter_child_one, entry); + AST_LIST_INSERT_TAIL(&test_counter.children, &test_counter_child_two, entry); + prometheus_metric_to_string(&test_counter, &buffer); + ast_test_validate(test, strcmp(ast_str_buffer(buffer), + "# HELP test_counter A test counter\n" + "# TYPE test_counter counter\n" + "test_counter 0\n" + "test_counter{key_one=\"value_one\",key_two=\"value_one\"} 0\n" + "test_counter{key_one=\"value_two\",key_two=\"value_two\"} 0\n") == 0); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(counter_create) +{ + RAII_VAR(struct prometheus_metric *, metric, NULL, prometheus_metric_free_wrapper); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test creation (and destruction) of malloc'd counters"; + info->description = + "This test covers creating a counter metric and destroying\n" + "it. The metric should be malloc'd."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + metric = prometheus_counter_create("test_counter", "A test counter"); + ast_test_validate(test, metric != NULL); + ast_test_validate(test, metric->type == PROMETHEUS_METRIC_COUNTER); + ast_test_validate(test, metric->allocation_strategy = PROMETHEUS_METRIC_MALLOCD); + ast_test_validate(test, !strcmp(metric->help, "A test counter")); + ast_test_validate(test, !strcmp(metric->name, "test_counter")); + ast_test_validate(test, !strcmp(metric->value, "")); + ast_test_validate(test, metric->children.first == NULL); + ast_test_validate(test, metric->children.last == NULL); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(gauge_to_string) +{ + struct prometheus_metric test_gauge = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_GAUGE, + "test_gauge", + "A test gauge", + NULL); + struct prometheus_metric test_gauge_child_one = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_GAUGE, + "test_gauge", + "A test gauge", + NULL); + struct prometheus_metric test_gauge_child_two = PROMETHEUS_METRIC_STATIC_INITIALIZATION( + PROMETHEUS_METRIC_GAUGE, + "test_gauge", + "A test gauge", + NULL); + RAII_VAR(struct ast_str *, buffer, NULL, ast_free); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test formatting of gauges"; + info->description = + "This test covers the formatting of printed gauges"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + buffer = ast_str_create(128); + if (!buffer) { + return AST_TEST_FAIL; + } + + PROMETHEUS_METRIC_SET_LABEL(&test_gauge_child_one, 0, "key_one", "value_one"); + PROMETHEUS_METRIC_SET_LABEL(&test_gauge_child_one, 1, "key_two", "value_one"); + PROMETHEUS_METRIC_SET_LABEL(&test_gauge_child_two, 0, "key_one", "value_two"); + PROMETHEUS_METRIC_SET_LABEL(&test_gauge_child_two, 1, "key_two", "value_two"); + AST_LIST_INSERT_TAIL(&test_gauge.children, &test_gauge_child_one, entry); + AST_LIST_INSERT_TAIL(&test_gauge.children, &test_gauge_child_two, entry); + prometheus_metric_to_string(&test_gauge, &buffer); + ast_test_validate(test, strcmp(ast_str_buffer(buffer), + "# HELP test_gauge A test gauge\n" + "# TYPE test_gauge gauge\n" + "test_gauge 0\n" + "test_gauge{key_one=\"value_one\",key_two=\"value_one\"} 0\n" + "test_gauge{key_one=\"value_two\",key_two=\"value_two\"} 0\n") == 0); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(gauge_create) +{ + RAII_VAR(struct prometheus_metric *, metric, NULL, prometheus_metric_free_wrapper); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test creation (and destruction) of malloc'd gauges"; + info->description = + "This test covers creating a gauge metric and destroying\n" + "it. The metric should be malloc'd."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + metric = prometheus_gauge_create("test_gauge", "A test gauge"); + ast_test_validate(test, metric != NULL); + ast_test_validate(test, metric->type == PROMETHEUS_METRIC_GAUGE); + ast_test_validate(test, metric->allocation_strategy = PROMETHEUS_METRIC_MALLOCD); + ast_test_validate(test, !strcmp(metric->help, "A test gauge")); + ast_test_validate(test, !strcmp(metric->name, "test_gauge")); + ast_test_validate(test, !strcmp(metric->value, "")); + ast_test_validate(test, metric->children.first == NULL); + ast_test_validate(test, metric->children.last == NULL); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(config_general_basic_auth) +{ + RAII_VAR(CURL *, curl, NULL, curl_free_wrapper); + struct prometheus_general_config *config; + int res; + long response_code; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test basic auth handling"; + info->description = + "This test covers authentication of requests"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + config = config_alloc(); + if (!config) { + return AST_TEST_NOT_RUN; + } + ast_string_field_set(config, auth_username, "foo"); + ast_string_field_set(config, auth_password, "bar"); + /* Prometheus module owns the ref after this call */ + prometheus_general_config_set(config); + ao2_ref(config, -1); + + curl = get_curl_instance(); + if (!curl) { + return AST_TEST_NOT_RUN; + } + + ast_test_status_update(test, "Testing without auth credentials\n"); + ast_test_status_update(test, " -> CURLing request...\n"); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + return AST_TEST_FAIL; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + ast_test_status_update(test, " -> CURL returned %ld\n", response_code); + ast_test_validate(test, response_code == 401); + + ast_test_status_update(test, "Testing with invalid auth credentials\n"); + ast_test_status_update(test, " -> CURLing request...\n"); + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl, CURLOPT_USERPWD, "matt:jordan"); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + return AST_TEST_FAIL; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + ast_test_status_update(test, " -> CURL returned %ld\n", response_code); + ast_test_validate(test, response_code == 401); + + ast_test_status_update(test, "Testing with valid auth credentials\n"); + ast_test_status_update(test, " -> CURLing request...\n"); + curl_easy_setopt(curl, CURLOPT_USERPWD, "foo:bar"); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + return AST_TEST_FAIL; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + ast_test_status_update(test, " -> CURL returned %ld\n", response_code); + ast_test_validate(test, response_code == 200); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(config_general_enabled) +{ + RAII_VAR(CURL *, curl, NULL, curl_free_wrapper); + struct prometheus_general_config *config; + int res; + long response_code; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test handling of enable/disable"; + info->description = + "When disabled, the module should return a 503.\n" + "This test verifies that it actually occurs."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + config = config_alloc(); + if (!config) { + return AST_TEST_NOT_RUN; + } + config->enabled = 0; + /* Prometheus module owns the ref after this call */ + prometheus_general_config_set(config); + ao2_ref(config, -1); + + curl = get_curl_instance(); + if (!curl) { + return AST_TEST_NOT_RUN; + } + + ast_test_status_update(test, " -> CURLing request...\n"); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + return AST_TEST_FAIL; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + ast_test_status_update(test, " -> CURL returned %ld\n", response_code); + ast_test_validate(test, response_code == 503); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(config_general_core_metrics) +{ + RAII_VAR(CURL *, curl, NULL, curl_free_wrapper); + RAII_VAR(struct ast_str *, buffer, NULL, ast_free); + struct prometheus_general_config *config; + int res; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test producing core metrics"; + info->description = + "This test covers the core metrics that are produced\n" + "by the basic Prometheus module."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + buffer = ast_str_create(128); + if (!buffer) { + return AST_TEST_NOT_RUN; + } + + config = config_alloc(); + if (!config) { + return AST_TEST_NOT_RUN; + } + config->core_metrics_enabled = 1; + /* Prometheus module owns the ref after this call */ + prometheus_general_config_set(config); + ao2_ref(config, -1); + + curl = get_curl_instance(); + if (!curl) { + return AST_TEST_NOT_RUN; + } + + ast_test_status_update(test, " -> CURLing request...\n"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_string_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + ast_test_status_update(test, "Failed to execute CURL: %d\n", res); + return AST_TEST_FAIL; + } + ast_test_status_update(test, " -> Retrieved: %s\n", ast_str_buffer(buffer)); + + ast_test_status_update(test, " -> Checking for core properties\n"); + ast_test_validate(test, strstr(ast_str_buffer(buffer), "asterisk_core_properties") != NULL); + + ast_test_status_update(test, " -> Checking for uptime\n"); + ast_test_validate(test, strstr(ast_str_buffer(buffer), "asterisk_core_uptime_seconds") != NULL); + + ast_test_status_update(test, " -> Checking for last reload\n"); + ast_test_validate(test, strstr(ast_str_buffer(buffer), "asterisk_core_last_reload_seconds") != NULL); + + ast_test_status_update(test, " -> Checking for scrape time\n"); + ast_test_validate(test, strstr(ast_str_buffer(buffer), "asterisk_core_scrape_time_ms") != NULL); + + return AST_TEST_PASS; +} + +static int process_config(int reload) +{ + struct ast_config *config; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + const char *bindaddr; + const char *bindport; + const char *prefix; + const char *enabled; + + config = ast_config_load("http.conf", config_flags); + if (!config || config == CONFIG_STATUS_FILEINVALID) { + ast_log(AST_LOG_NOTICE, "HTTP config file is invalid; declining load"); + return -1; + } else if (config == CONFIG_STATUS_FILEUNCHANGED) { + return 0; + } + + enabled = ast_config_option(config, "general", "enabled"); + if (!enabled || ast_false(enabled)) { + ast_config_destroy(config); + ast_log(AST_LOG_NOTICE, "HTTP server is disabled; declining load"); + return -1; + } + + /* Construct our Server URI */ + bindaddr = ast_config_option(config, "general", "bindaddr"); + if (!bindaddr) { + ast_config_destroy(config); + ast_log(AST_LOG_NOTICE, "HTTP config file fails to specify 'bindaddr'; declining load"); + return -1; + } + + bindport = ast_config_option(config, "general", "bindport"); + if (!bindport) { + bindport = "8088"; + } + + prefix = ast_config_option(config, "general", "prefix"); + + snprintf(server_uri, sizeof(server_uri), "http://%s:%s%s/test_metrics", bindaddr, bindport, S_OR(prefix, "")); + + ast_config_destroy(config); + + return 0; +} + +static int test_init_cb(struct ast_test_info *info, struct ast_test *test) +{ + struct prometheus_general_config *new_module_config; + + new_module_config = config_alloc(); + if (!new_module_config) { + return -1; + } + + module_config = prometheus_general_config_get(); + prometheus_general_config_set(new_module_config); + + /* Allow the module to own the ref */ + ao2_ref(new_module_config, -1); + + return 0; +} + +static int test_cleanup_cb(struct ast_test_info *info, struct ast_test *test) +{ + prometheus_general_config_set(module_config); + ao2_cleanup(module_config); + + return 0; +} + +static int reload_module(void) +{ + return process_config(1); +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(metric_values); + AST_TEST_UNREGISTER(metric_callback_register); + AST_TEST_UNREGISTER(metric_register); + + AST_TEST_UNREGISTER(counter_to_string); + AST_TEST_UNREGISTER(counter_create); + AST_TEST_UNREGISTER(gauge_to_string); + AST_TEST_UNREGISTER(gauge_create); + + AST_TEST_UNREGISTER(config_general_enabled); + AST_TEST_UNREGISTER(config_general_basic_auth); + AST_TEST_UNREGISTER(config_general_core_metrics); + + return 0; +} + +static int load_module(void) +{ + if (process_config(0)) { + return AST_MODULE_LOAD_DECLINE; + } + + AST_TEST_REGISTER(metric_values); + AST_TEST_REGISTER(metric_callback_register); + AST_TEST_REGISTER(metric_register); + + AST_TEST_REGISTER(counter_to_string); + AST_TEST_REGISTER(counter_create); + AST_TEST_REGISTER(gauge_to_string); + AST_TEST_REGISTER(gauge_create); + + AST_TEST_REGISTER(config_general_enabled); + AST_TEST_REGISTER(config_general_basic_auth); + AST_TEST_REGISTER(config_general_core_metrics); + + ast_test_register_init(CATEGORY, &test_init_cb); + ast_test_register_cleanup(CATEGORY, &test_cleanup_cb); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Prometheus Core Unit Tests", + .load = load_module, + .reload = reload_module, + .unload = unload_module, + .requires = "res_prometheus", +);