mirror of https://github.com/asterisk/asterisk
				
				
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							1044 lines
						
					
					
						
							27 KiB
						
					
					
				
			
		
		
	
	
							1044 lines
						
					
					
						
							27 KiB
						
					
					
				| /*
 | |
|  * 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 Core Prometheus metrics API
 | |
|  *
 | |
|  * \author Matt Jordan <mjordan@digium.com>
 | |
|  *
 | |
|  */
 | |
| 
 | |
| /*** MODULEINFO
 | |
| 	<use>pjproject</use>
 | |
| 	<use type="module">res_pjsip</use>
 | |
| 	<use type="module">res_pjsip_outbound_registration</use>
 | |
| 	<support_level>extended</support_level>
 | |
|  ***/
 | |
| 
 | |
| /*** DOCUMENTATION
 | |
| 	<configInfo name="res_prometheus" language="en_US">
 | |
| 		<synopsis>Resource for integration with Prometheus</synopsis>
 | |
| 		<configFile name="prometheus.conf">
 | |
| 			<configObject name="general">
 | |
| 				<since>
 | |
| 					<version>17.0.0</version>
 | |
| 				</since>
 | |
| 				<synopsis>General settings.</synopsis>
 | |
| 				<description>
 | |
| 					<para>
 | |
| 					The <emphasis>general</emphasis> settings section contains information
 | |
| 					to configure Asterisk to serve up statistics for a Prometheus server.
 | |
| 					</para>
 | |
| 					<note>
 | |
| 						<para>You must enable Asterisk's HTTP server in <filename>http.conf</filename>
 | |
| 						for this module to function properly!
 | |
| 						</para>
 | |
| 					</note>
 | |
| 				</description>
 | |
| 				<configOption name="enabled" default="no">
 | |
| 					<since>
 | |
| 						<version>17.0.0</version>
 | |
| 					</since>
 | |
| 					<synopsis>Enable or disable Prometheus statistics.</synopsis>
 | |
| 					<description>
 | |
| 						<enumlist>
 | |
| 							<enum name="no" />
 | |
| 							<enum name="yes" />
 | |
| 						</enumlist>
 | |
| 					</description>
 | |
| 				</configOption>
 | |
| 				<configOption name="core_metrics_enabled" default="yes">
 | |
| 					<since>
 | |
| 						<version>17.0.0</version>
 | |
| 					</since>
 | |
| 					<synopsis>Enable or disable core metrics.</synopsis>
 | |
| 					<description>
 | |
| 						<para>
 | |
| 						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.
 | |
| 						</para>
 | |
| 						<enumlist>
 | |
| 							<enum name="no" />
 | |
| 							<enum name="yes" />
 | |
| 						</enumlist>
 | |
| 					</description>
 | |
| 				</configOption>
 | |
| 				<configOption name="uri" default="metrics">
 | |
| 					<since>
 | |
| 						<version>17.0.0</version>
 | |
| 					</since>
 | |
| 					<synopsis>The HTTP URI to serve metrics up on.</synopsis>
 | |
| 				</configOption>
 | |
| 				<configOption name="auth_username">
 | |
| 					<since>
 | |
| 						<version>17.0.0</version>
 | |
| 					</since>
 | |
| 					<synopsis>Username to use for Basic Auth.</synopsis>
 | |
| 					<description>
 | |
| 						<para>
 | |
| 						If set, use Basic Auth to authenticate requests to the route
 | |
| 						specified by <replaceable>uri</replaceable>. Note that you
 | |
| 						will need to configure your Prometheus server with the
 | |
| 						appropriate auth credentials.
 | |
| 						</para>
 | |
| 						<para>
 | |
| 						If set, <replaceable>auth_password</replaceable> must also
 | |
| 						be set appropriately.
 | |
| 						</para>
 | |
| 						<warning>
 | |
| 							<para>
 | |
| 							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.
 | |
| 							</para>
 | |
| 						</warning>
 | |
| 					</description>
 | |
| 				</configOption>
 | |
| 				<configOption name="auth_password">
 | |
| 					<since>
 | |
| 						<version>17.0.0</version>
 | |
| 					</since>
 | |
| 					<synopsis>Password to use for Basic Auth.</synopsis>
 | |
| 					<description>
 | |
| 						<para>
 | |
| 						If set, this is used in conjunction with <replaceable>auth_username</replaceable>
 | |
| 						to require Basic Auth for all requests to the Prometheus metrics. Note that
 | |
| 						setting this without <replaceable>auth_username</replaceable> will not
 | |
| 						do anything.
 | |
| 						</para>
 | |
| 					</description>
 | |
| 				</configOption>
 | |
| 				<configOption name="auth_realm" default="Asterisk Prometheus Metrics">
 | |
| 					<since>
 | |
| 						<version>17.0.0</version>
 | |
| 					</since>
 | |
| 					<synopsis>Auth realm used in challenge responses</synopsis>
 | |
| 				</configOption>
 | |
| 			</configObject>
 | |
| 		</configFile>
 | |
| 	</configInfo>
 | |
| ***/
 | |
| 
 | |
| #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"
 | |
| 
 | |
| #include "prometheus/prometheus_internal.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;
 | |
| 
 | |
| AST_VECTOR(, const struct prometheus_metrics_provider *) providers;
 | |
| 
 | |
| static struct timeval last_scrape;
 | |
| 
 | |
| /*! \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 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 void scrape_metrics(struct ast_str **response)
 | |
| {
 | |
| 	int i;
 | |
| 
 | |
| 	for (i = 0; i < AST_VECTOR_SIZE(&callbacks); i++) {
 | |
| 		struct prometheus_callback *callback = AST_VECTOR_GET(&callbacks, i);
 | |
| 
 | |
| 		if (!callback) {
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		callback->callback_fn(response);
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < AST_VECTOR_SIZE(&metrics); i++) {
 | |
| 		struct prometheus_metric *metric = AST_VECTOR_GET(&metrics, i);
 | |
| 
 | |
| 		if (!metric) {
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		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);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 ast_str *content_type_header = NULL;
 | |
| 	struct timeval start;
 | |
| 	struct timeval end;
 | |
| 
 | |
| 	/* 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);
 | |
| 	content_type_header = ast_str_create(32);
 | |
| 	if (!response || !content_type_header) {
 | |
| 		goto err500;
 | |
| 	}
 | |
| 
 | |
| 	ast_str_set(&content_type_header, 0, "Content-Type: text/plain\r\n");
 | |
| 
 | |
| 	start = ast_tvnow();
 | |
| 
 | |
| 	ast_mutex_lock(&scrape_lock);
 | |
| 
 | |
| 	last_scrape = start;
 | |
| 	scrape_metrics(&response);
 | |
| 
 | |
| 	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", content_type_header, 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);
 | |
| 	ast_free(content_type_header);
 | |
| 	return 0;
 | |
| err503:
 | |
| 	ast_http_send(ser, method, 503, "Service Unavailable", NULL, NULL, 0, 1);
 | |
| 	ast_free(response);
 | |
| 	ast_free(content_type_header);
 | |
| 	return 0;
 | |
| err500:
 | |
| 	ast_http_send(ser, method, 500, "Server Error", NULL, NULL, 0, 1);
 | |
| 	ast_free(response);
 | |
| 	ast_free(content_type_header);
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| struct ast_str *prometheus_scrape_to_string(void)
 | |
| {
 | |
| 	struct ast_str *response;
 | |
| 
 | |
| 	response = ast_str_create(512);
 | |
| 	if (!response) {
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	ast_mutex_lock(&scrape_lock);
 | |
| 	scrape_metrics(&response);
 | |
| 	ast_mutex_unlock(&scrape_lock);
 | |
| 
 | |
| 	return response;
 | |
| }
 | |
| 
 | |
| int64_t prometheus_last_scrape_duration_get(void)
 | |
| {
 | |
| 	int64_t duration;
 | |
| 
 | |
| 	if (sscanf(core_scrape_metric.value, "%" PRIu64, &duration) != 1) {
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	return duration;
 | |
| }
 | |
| 
 | |
| struct timeval prometheus_last_scrape_time_get(void)
 | |
| {
 | |
| 	SCOPED_MUTEX(lock, &scrape_lock);
 | |
| 
 | |
| 	return last_scrape;
 | |
| }
 | |
| 
 | |
| 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]);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void prometheus_metrics_provider_register(const struct prometheus_metrics_provider *provider)
 | |
| {
 | |
| 	AST_VECTOR_APPEND(&providers, provider);
 | |
| }
 | |
| 
 | |
| 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(&providers); i++) {
 | |
| 		const struct prometheus_metrics_provider *provider = AST_VECTOR_GET(&providers, i);
 | |
| 
 | |
| 		if (!provider->unload_cb) {
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		provider->unload_cb();
 | |
| 	}
 | |
| 
 | |
| 	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);
 | |
| 
 | |
| 	AST_VECTOR_FREE(&providers);
 | |
| 
 | |
| 	aco_info_destroy(&cfg_info);
 | |
| 	ao2_global_obj_release(global_config);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static int reload_module(void) {
 | |
| 	SCOPED_MUTEX(lock, &scrape_lock);
 | |
| 	int i;
 | |
| 	struct prometheus_general_config *general_config;
 | |
| 
 | |
| 	ast_http_uri_unlink(&prometheus_uri);
 | |
| 	if (aco_process_config(&cfg_info, 1) == ACO_PROCESS_ERROR) {
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	/* Our config should be all reloaded now */
 | |
| 	general_config = prometheus_general_config_get();
 | |
| 	for (i = 0; i < AST_VECTOR_SIZE(&providers); i++) {
 | |
| 		const struct prometheus_metrics_provider *provider = AST_VECTOR_GET(&providers, i);
 | |
| 
 | |
| 		if (!provider->reload_cb) {
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (provider->reload_cb(general_config)) {
 | |
| 			ast_log(AST_LOG_WARNING, "Failed to reload metrics provider %s\n", provider->name);
 | |
| 			ao2_ref(general_config, -1);
 | |
| 			return -1;
 | |
| 		}
 | |
| 	}
 | |
| 	ao2_ref(general_config, -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 (AST_VECTOR_INIT(&providers, 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 (cli_init()
 | |
| 		|| channel_metrics_init()
 | |
| 		|| endpoint_metrics_init()
 | |
| 		|| bridge_metrics_init()) {
 | |
| 		goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	if(ast_module_check("res_pjsip_outbound_registration.so")) {
 | |
| 		/* Call a local function, used in the core prometheus code only */
 | |
| 		if (pjsip_outbound_registration_metrics_init())
 | |
| 		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);
 | |
| 	AST_VECTOR_FREE(&providers);
 | |
| 
 | |
| 	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,
 | |
| #ifdef HAVE_PJPROJECT
 | |
| 	/* This module explicitly calls into res_pjsip if Asterisk is built with PJSIP support, so they are required. */
 | |
| 	.requires = "res_pjsip",
 | |
| 	.optional_modules = "res_pjsip_outbound_registration",
 | |
| #endif
 | |
| );
 |