mirror of https://github.com/asterisk/asterisk
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
8.2 KiB
308 lines
8.2 KiB
/*
|
|
* Asterisk -- An open source telephony toolkit.
|
|
*
|
|
* Copyright (C) 2026, Sangoma Technologies Corporation
|
|
*
|
|
* George Joseph <gjoseph@sangoma.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
|
|
* \author George Joseph <gjoseph@sangoma.com>
|
|
*
|
|
* \brief Common log entrypoint from the cdr/cel modules
|
|
*
|
|
*/
|
|
|
|
#include "cdrel.h"
|
|
#include "asterisk/pbx.h"
|
|
#include "asterisk/vector.h"
|
|
|
|
/*
|
|
* We can save some time and ast_str memory allocation work by allocating a single
|
|
* thread-local buffer and re-using it.
|
|
*/
|
|
AST_THREADSTORAGE(custom_buf);
|
|
|
|
/*!
|
|
* \internal
|
|
* \brief Free an ast_value object
|
|
*
|
|
* ... and if the data type is "string", free it as well.
|
|
*
|
|
* \param data
|
|
*/
|
|
static void free_value(void *data)
|
|
{
|
|
struct cdrel_value *val = data;
|
|
if (val->data_type == cdrel_type_string) {
|
|
ast_free(val->values.string);
|
|
val->values.string = NULL;
|
|
}
|
|
ast_free(val);
|
|
}
|
|
|
|
/*!
|
|
* \internal
|
|
* \brief Free a vector of cdrel_values
|
|
*
|
|
* \param data
|
|
*/
|
|
static void free_value_vector(void *data)
|
|
{
|
|
struct cdrel_values *values = data;
|
|
AST_VECTOR_RESET(values, free_value);
|
|
AST_VECTOR_PTR_FREE(values);
|
|
}
|
|
|
|
/*!
|
|
* \internal
|
|
* \brief Log a legacy record to a file.
|
|
*
|
|
* The file legacy format specifies one long string with dialplan functions. We have no idea
|
|
* what the separator is so we need to pass the entire string to ast_str_substitute_variables.
|
|
* This is where the cycles are spent. We then write the result directly to the backend
|
|
* file bypassing all of the advanced processing.
|
|
*
|
|
* \param config The configuration object.
|
|
* \param data The data to write. May be an ast_cdr or an ast_event.
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int log_legacy_dsv_record(struct cdrel_config *config, void *data)
|
|
{
|
|
struct ast_channel *dummy = data;
|
|
struct ast_str *str;
|
|
|
|
if (!(str = ast_str_thread_get(&custom_buf, 1024))) {
|
|
return -1;
|
|
}
|
|
ast_str_reset(str);
|
|
|
|
ast_str_substitute_variables(&str, 0, dummy, config->template);
|
|
|
|
return write_record_to_file(config, str);
|
|
}
|
|
|
|
/*!
|
|
* \internal
|
|
* \brief Log a legacy record to a database.
|
|
*
|
|
* Unlike the file backends, the legacy database backend configs always use commas
|
|
* as field separators but they all still use dialplan functions so we need still
|
|
* need to do evaluation and substitution. Since we know the separator however,
|
|
* we can iterate over the individual fields.
|
|
*
|
|
* \param config The configuration object.
|
|
* \param data The data to write. May be an ast_cdr or an ast_event.
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int log_legacy_database_record(struct cdrel_config *config, void *data)
|
|
{
|
|
struct ast_channel *dummy = data;
|
|
int ix = 0;
|
|
int res = 0;
|
|
char subst_buf[2048];
|
|
size_t field_count = AST_VECTOR_SIZE(&config->fields);
|
|
RAII_VAR(struct cdrel_values *, values, ast_calloc(1, sizeof(*values)), free_value_vector);
|
|
|
|
if (!values) {
|
|
return -1;
|
|
}
|
|
|
|
res = AST_VECTOR_INIT(values, field_count);
|
|
if (res != 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (config->db == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
for (ix = 0; ix < AST_VECTOR_SIZE(&config->fields); ix++) {
|
|
struct cdrel_field *field = AST_VECTOR_GET(&config->fields, ix);
|
|
struct cdrel_value *output_value = ast_calloc(1, sizeof(*output_value));
|
|
|
|
if (!output_value) {
|
|
return -1;
|
|
}
|
|
output_value->mallocd = 1;
|
|
|
|
pbx_substitute_variables_helper(dummy, field->data, subst_buf, sizeof(subst_buf) - 1);
|
|
output_value->data_type = cdrel_type_string;
|
|
|
|
output_value->field_name = field->name;
|
|
output_value->values.string = ast_strdup(ast_strip_quoted(subst_buf, "'\"", "'\""));
|
|
if (!output_value->values.string) {
|
|
return -1;
|
|
}
|
|
|
|
res = AST_VECTOR_APPEND(values, output_value);
|
|
if (res != 0) {
|
|
ast_free(output_value);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return write_record_to_database(config, values);
|
|
}
|
|
|
|
/*!
|
|
* \internal
|
|
* \brief Log an advanced record
|
|
*
|
|
* For the file advanced formats, we know what the field separator is so we
|
|
* iterate over them and accumulate the results in a vector of cdrel_values.
|
|
* No dialplan function evaluation needed.
|
|
*
|
|
* \param config The configuration object.
|
|
* \param data The data to log. May be an ast_cdr or an ast_event.
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int log_advanced_record(struct cdrel_config *config, void *data)
|
|
{
|
|
int ix = 0;
|
|
int res = 0;
|
|
size_t field_count = AST_VECTOR_SIZE(&config->fields);
|
|
RAII_VAR(struct cdrel_values *, values, ast_calloc(1, sizeof(*values)), free_value_vector);
|
|
|
|
if (!values) {
|
|
return -1;
|
|
}
|
|
|
|
res = AST_VECTOR_INIT(values, field_count);
|
|
if (res != 0) {
|
|
return -1;
|
|
}
|
|
|
|
for (ix = 0; ix < AST_VECTOR_SIZE(&config->fields); ix++) {
|
|
struct cdrel_field *field = AST_VECTOR_GET(&config->fields, ix);
|
|
struct cdrel_value input_value = { 0, };
|
|
struct cdrel_value *output_value = ast_calloc(1, sizeof(*output_value));
|
|
|
|
if (!output_value) {
|
|
return -1;
|
|
}
|
|
output_value->mallocd = 1;
|
|
|
|
/*
|
|
* Get a field from a CDR structure or CEL event into an cdrel_value.
|
|
*/
|
|
res = cdrel_field_getters[config->record_type][field->input_data_type](data, config, field, &input_value);
|
|
if (res != 0) {
|
|
ast_free(output_value);
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Set the output data type to the type we want to see in the output.
|
|
*/
|
|
output_value->data_type = field->output_data_type;
|
|
|
|
/*
|
|
* Now call the formatter based on the INPUT data type.
|
|
*/
|
|
res = cdrel_field_formatters[input_value.data_type](config, field, &input_value, output_value);
|
|
if (res != 0) {
|
|
ast_free(output_value);
|
|
return -1;
|
|
}
|
|
|
|
res = AST_VECTOR_APPEND(values, output_value);
|
|
if (res != 0) {
|
|
ast_free(output_value);
|
|
return -1;
|
|
}
|
|
}
|
|
return cdrel_backend_writers[config->format_type](config, values);
|
|
}
|
|
|
|
/*
|
|
* These callbacks are only used in this file so there's no need to
|
|
* make them available to the rest of the module.
|
|
*/
|
|
typedef int (*cdrel_logger_cb)(struct cdrel_config *config, void *data);
|
|
|
|
static const cdrel_logger_cb logger_callbacks[cdrel_backend_type_end][cdrel_config_type_end] = {
|
|
[cdrel_backend_text] = {
|
|
[cdrel_config_legacy] = log_legacy_dsv_record,
|
|
[cdrel_config_advanced] = log_advanced_record
|
|
},
|
|
[cdrel_backend_db] = {
|
|
[cdrel_config_legacy] = log_legacy_database_record,
|
|
[cdrel_config_advanced] = log_advanced_record
|
|
},
|
|
};
|
|
|
|
/*!
|
|
* \internal
|
|
* \brief Main logging entrypoint from the individual modules.
|
|
*
|
|
* This is the entrypoint from the individual cdr and cel modules.
|
|
* "data" will either be an ast_cdr or ast_event structure but we
|
|
* don't actually care at this point.
|
|
*
|
|
* For legacy configs, we need to create a dummy channel so we'll
|
|
* do that if/when we hit the first one and we'll reuse it for all
|
|
* further legacy configs. If we fail to get a channel, we'll skip
|
|
* all further configs.
|
|
*
|
|
* \warning This function MUST be called with the module's config_lock
|
|
* held for reading to prevent reloads from happening while we're logging.
|
|
*
|
|
* \param configs The calling module's vector of configuration objects.
|
|
* \param data The data to write. May be an ast_cdr or an ast_event.
|
|
* \retval 0 on success
|
|
* \retval <0 on failure. The magnitude indicates how many configs failed.
|
|
*/
|
|
int cdrel_logger(struct cdrel_configs *configs, void *data)
|
|
{
|
|
struct ast_channel *dummy = NULL;
|
|
int ix = 0;
|
|
int skip_legacy = 0;
|
|
int res = 0;
|
|
|
|
for(ix = 0; ix < AST_VECTOR_SIZE(configs); ix++) {
|
|
struct cdrel_config *config = AST_VECTOR_GET(configs, ix);
|
|
void *chan_or_data = NULL;
|
|
|
|
if (config->config_type == cdrel_config_legacy) {
|
|
if (skip_legacy) {
|
|
continue;
|
|
}
|
|
if (!dummy) {
|
|
dummy = config->dummy_channel_alloc(config, data);
|
|
if (!dummy) {
|
|
ast_log(LOG_ERROR, "Unable to fabricate channel from CEL event for '%s'\n",
|
|
config->output_filename);
|
|
skip_legacy = 1;
|
|
res--;
|
|
continue;
|
|
}
|
|
}
|
|
chan_or_data = dummy;
|
|
} else {
|
|
chan_or_data = data;
|
|
}
|
|
res += logger_callbacks[config->backend_type][config->config_type](config, chan_or_data);
|
|
}
|
|
|
|
if (dummy) {
|
|
ast_channel_unref(dummy);
|
|
}
|
|
return res;
|
|
}
|
|
|