Add core Prometheus support to Asterisk
authorMatt Jordan <mjordan@digium.com>
Thu, 3 Jan 2019 16:28:28 +0000 (10:28 -0600)
committerMatt Jordan <mjordan@digium.com>
Tue, 21 May 2019 01:33:58 +0000 (20:33 -0500)
Prometheus is the defacto monitoring tool for containerized applications.
This patch adds native support to Asterisk for serving up Prometheus
compatible metrics, such that a Prometheus server can scrape an Asterisk
instance in the same fashion as it does other HTTP services.

The core module in this patch provides an API that future work can build
on top of. The API manages metrics in one of two ways:
(1) Registered metrics. In this particular case, the API assumes that
    the metric (either allocated on the stack or on the heap) will have
    its value updated by the module registering it at will, and not
    just when Prometheus scrapes Asterisk. When a scrape does occur,
    the metrics are locked so that the current value can be retrieved.
(2) Scrape callbacks. In this case, the API allows consumers to be
    called via a callback function when a Prometheus initiated scrape
    occurs. The consumers of the API are responsible for populating
    the response to Prometheus themselves, typically using stack
    allocated metrics that are then formatted properly into strings
    via this module's convenience functions.

These two mechanisms balance the different ways in which information is
generated within Asterisk: some information is generated in a fashion
that makes it appropriate to update the relevant metrics immediately;
some information is better to defer until a Prometheus server asks for
it.

Note that some care has been taken in how metrics are defined to
minimize the impact on performance. Prometheus's metric definition
and its support for nesting metrics based on labels - which are
effectively key/value pairs - can make storage and managing of metrics
somewhat tricky. While a naive approach, where we allow for any number
of labels and perform a lot of heap allocations to manage the information,
would absolutely have worked, this patch instead opts to try to place
as much information in length limited arrays, stack allocations, and
vectors to minimize the performance impacts of scrapes. The author of
this patch has worked on enough systems that were driven to their knees
by poor monitoring implementations to be a bit cautious.

Additionally, this patch only adds support for gauges and counters.
Additional work to add summaries, histograms, and other Prometheus
metric types may add value in the future. This would be of particular
interest if someone wanted to track SIP response types.

Finally, this patch includes unit tests for the core APIs.

ASTERISK-28403

Change-Id: I891433a272c92fd11c705a2c36d65479a415ec42

configs/samples/prometheus.conf.sample [new file with mode: 0644]
include/asterisk/res_prometheus.h [new file with mode: 0644]
res/res_prometheus.c [new file with mode: 0644]
res/res_prometheus.exports.in [new file with mode: 0644]
tests/test_res_prometheus.c [new file with mode: 0644]

diff --git a/configs/samples/prometheus.conf.sample b/configs/samples/prometheus.conf.sample
new file mode 100644 (file)
index 0000000..63e9bd6
--- /dev/null
@@ -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 (file)
index 0000000..cf62b7b
--- /dev/null
@@ -0,0 +1,478 @@
+/*
+ * res_prometheus: Asterisk Prometheus Metrics
+ *
+ * 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.
+ */
+
+#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 (file)
index 0000000..1f4e635
--- /dev/null
@@ -0,0 +1,899 @@
+/*
+ * 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
+       <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">
+                               <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">
+                                       <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">
+                                       <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">
+                                       <synopsis>The HTTP URI to serve metrics up on.</synopsis>
+                               </configOption>
+                               <configOption name="auth_username">
+                                       <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">
+                                       <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">
+                                       <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"
+
+/*! \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 (file)
index 0000000..cec31c6
--- /dev/null
@@ -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 (file)
index 0000000..01279be
--- /dev/null
@@ -0,0 +1,829 @@
+/*
+ * 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.
+ */
+
+/*** MODULEINFO
+       <depend>TEST_FRAMEWORK</depend>
+       <depend>res_prometheus</depend>
+       <depend>curl</depend>
+       <support_level>extended</support_level>
+ ***/
+
+#include "asterisk.h"
+
+#include <curl/curl.h>
+
+#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",
+);