mirror of https://github.com/asterisk/asterisk
commit
12cad5ec1a
@ -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
|
@ -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__ */
|
@ -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,
|
||||
);
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
global:
|
||||
LINKER_SYMBOL_PREFIXprometheus*;
|
||||
local:
|
||||
*;
|
||||
};
|
@ -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",
|
||||
);
|
Loading…
Reference in new issue