mirror of https://github.com/asterisk/asterisk
There is a LOT of work in this commit but the TL;DR is that it takes CEL processing from using 38% of the CPU instructions used by a call, which is more than that used by the call processing itself, down to less than 10% of the instructions. So here's the deal... cdr_custom, cdr_sqlite3_custom, cel_custom and cel_sqlite3_custom all shared one ugly trait...they all used ast_str_substitute_variables() or pbx_substitute_variables_helper() to resolve the dialplan functions used in their config files. Not only are they both extraordinarily expensive, they both require a dummy channel to be allocated and destroyed for each record written. For CDRs, that's not too bad because we only write one CDR per call. For CELs however, it's a disaster. As far as source code goes, the modules basically all did the same thing. Unfortunately, they did it badly. The config files simply contained long opaque strings which were intepreted by ast_str_substitute_variables() or pbx_substitute_variables_helper(), the very functions that ate all the instructions. This meant introducing a new "advanced" config format much like the advanced manager event filtering added to manager.conf in 2024. Fortunately however, if the legacy config was recognizable, we were able to parse it as an advanced config and gain the benefit. If not, then it goes the legacy, and very expensive, route. Given the commonality among the modules, instead of making the same improvements to 4 modules then trying to maintain them over time, a single module "res_cdrel_custom" was created that contains all of the common code. A few bonuses became possible in the process... * The cdr_custom and cel_custom modules now support JSON formatted output. * The cdr_sqlite_custom and cel_sqlite3_custom modules no longer have to share an Sqlite3 database. Summary of changes: A new module "res/res_cdrel_custom.c" has been created and the existing cdr_custom, cdr_sqlite3_custom, cel_custom and cel_sqlite3_custom modules are now just stubs that call the code in res_cdrel_custom. res_cdrel_custom contains: * A common configuration facility. * Getters for both CDR and CEL fields that share the same abstraction. * Formatters for all data types found in the ast_cdr and ast_event structures that share the same abstraction. * Common writers for the text file and database backends that, you guessed it, share the same abstraction. The result is that while there is certainly a net increase in the number of lines in the code base, most of it is in the configuration handling at load-time. The run-time instruction path length is now significanty shorter. ``` Scenario Instructions Latency ===================================================== CEL pre changes 38.49% 37.51% CEL Advanced 9.68% 6.06% CEL Legacy (auto-conv to adv) 9.95% 6.13% CEL Sqlite3 pre changes 39.41% 39.90% CEL Sqlite3 Advanced 25.68% 24.24% CEL Sqlite3 Legacy (auto conv) 25.88% 24.53% CDR pre changes 4.79% 2.95% CDR Advanced 0.79% 0.47% CDR Legacy (auto conv to adv) 0.86% 0.51% CDR Sqlite3 pre changes 4.47% 2.89% CEL Sqlite3 Advanced 2.16% 1.29% CEL Sqlite3 Legacy (auto conv) 2.19% 1.30% ``` Notes: * We only write one CDR per call but every little bit helps. * Sqlite3 still takes a fair amount of resources but the new config makes a decent improvement. * Legacy configs that we can't auto convert will still take the "pre changes" path. If you're interested in more implementation details, see the comments at the top of the res_cdrel_custom.c file. One minor fix to CEL is also included...Although TenantID was added to the ast_event structure, it was always rendered as an empty string. It's now properly rendered. UserNote: Significant performance improvements have been made to the cdr_custom, cdr_sqlite3_custom, cel_custom and cel_sqlite3_custom modules. See the new sample config files for those modules to see how to benefit from them.pull/1713/head
parent
f26cacb822
commit
fd781d966a
@ -1,11 +1,105 @@
|
||||
;
|
||||
; Mappings for custom config file
|
||||
; Asterisk Call Detail Record Logging (CDR) - Custom Sqlite3 Backend
|
||||
;
|
||||
[master] ; currently, only file "master.db" is supported, with only one table at a time.
|
||||
;table => cdr
|
||||
;columns => calldate, clid, dcontext, channel, dstchannel, lastapp, lastdata, duration, billsec, disposition, amaflags, accountcode, uniqueid, userfield, test
|
||||
;values => '${CDR(start)}','${CDR(clid)}','${CDR(dcontext)}','${CDR(channel)}','${CDR(dstchannel)}','${CDR(lastapp)}','${CDR(lastdata)}','${CDR(duration)}','${CDR(billsec)}','${CDR(disposition)}','${CDR(amaflags)}','${CDR(accountcode)}','${CDR(uniqueid)}','${CDR(userfield)}','${CDR(test)}'
|
||||
;busy_timeout => 1000
|
||||
|
||||
;Enable High Resolution Times for billsec and duration fields
|
||||
;values => '${CDR(start)}','${CDR(clid)}','${CDR(dcontext)}','${CDR(channel)}','${CDR(dstchannel)}','${CDR(lastapp)}','${CDR(lastdata)}','${CDR(duration,f)}','${CDR(billsec,f)}','${CDR(disposition)}','${CDR(amaflags)}','${CDR(accountcode)}','${CDR(uniqueid)}','${CDR(userfield)}','${CDR(test)}'
|
||||
; This is the configuration file for the customizable Sqlite3 backend for CDR
|
||||
; logging.
|
||||
;
|
||||
; Legacy vs Advanced Mappings
|
||||
;
|
||||
; Legacy mappings are those that are defined using dialplan functions like CDR
|
||||
; and CSV_QUOTE and require a VERY expensive function replacement process at
|
||||
; runtime for every record output.
|
||||
;
|
||||
; Advanced mappings are those that are defined by a simple list of field names
|
||||
; which uses significantly less resources at runtime.
|
||||
;
|
||||
;
|
||||
; Each category in this file represents a separate Sqlite3 database file.
|
||||
; A filename that starts with a forward-slash '/' will be treated as an absolute
|
||||
; path name. If it doesn't, it must be a file name with no directory separators
|
||||
; which will be placed in the /var/log/asterisk directory. If the database
|
||||
; file doesn't already exist, it will be created.
|
||||
;
|
||||
; Previous versions of Asterisk limited the output to a single database file
|
||||
; named "master" which was shared with CDR Sqlite3 logging. That is no longer
|
||||
; the case.
|
||||
;
|
||||
; Legacy Mappings
|
||||
;
|
||||
; Within a legacy mapping, use the CDR() and CSV_QUOTE() functions to retrieve
|
||||
; values from the CDR.
|
||||
;
|
||||
; NOTE: If your legacy mapping uses only the CDR dialplan function, the module
|
||||
; will attempt to strip the functions and create a much faster advanced mapping
|
||||
; for it. However, we urge you to create a real advanced mapping and not rely
|
||||
; on this process. If the mapping contains something not recognized it will go
|
||||
; the slower legacy route.
|
||||
;
|
||||
;
|
||||
;[cdr_master]
|
||||
;table = cdr ; The name of the table in the database into which records
|
||||
; are to be written. If the table doesn't already exist,
|
||||
; it will be created.
|
||||
|
||||
;columns = calldate, literal, clid, dcontext, channel, dstchannel, lastapp, lastdata, duration, billsec, disposition, amaflags, accountcode, uniqueid, userfield, ds_store
|
||||
; The column names to receive the fields. If the table doesn't already exist,
|
||||
; it will be created with these columns. If the table does exist, this list
|
||||
; MUST match the existing columns or the config will fail to load.
|
||||
; The column names do NOT have to match the field names however.
|
||||
|
||||
;values = '${CDR(start)}','some literal','${CDR(clid)}','${CDR(dcontext)}','${CDR(channel)}','${CDR(dstchannel)}','${CDR(lastapp)}','${CDR(lastdata)}','${CDR(duration)}','${CDR(billsec)}','${CDR(disposition)}','${CDR(amaflags)}','${CDR(accountcode)}','${CDR(uniqueid)}','${CDR(userfield)}','${CDR(ds_store)}'
|
||||
; The list of fields to write into the columns.
|
||||
; Each field MUST be enclosed in single-quotes and the fields separated
|
||||
; by commas. Additionally, the number of fields specified MUST match the
|
||||
; number of columns or the config will fail to load.
|
||||
|
||||
;busy_timeout = 1000 ; The number of milliseconds to wait for a database operation
|
||||
; to complete before an error is returned.
|
||||
|
||||
;
|
||||
; Advanced Mappings
|
||||
;
|
||||
;[cdr_advanced]
|
||||
;table = cdr ; The name of the table in the database into which records
|
||||
; are to be written. If the table doesn't already exist,
|
||||
; it will be created.
|
||||
|
||||
;columns = calldate, literal, clid, dcontext, channel, dstchannel, lastapp, lastdata, duration, billsec, disposition, amaflags, accountcode, uniqueid, userfield, ds_storetest ; The column names to receive the fields. If the table doesn't already exist,
|
||||
; it will be created with these columns. If the table does exist, this list
|
||||
; MUST match the existing columns or the config will fail to load.
|
||||
|
||||
;fields = start,"some literal",clid,dcontext,channel,dstchannel,lastapp,lastdata,duration,billsec,disposition,amaflags,accountcode,uniqueid,userfield,ds_store(uservar)
|
||||
; The "fields" parameter differentiates this mapping as an Advanced one
|
||||
; as opposed to "values" used above.
|
||||
;
|
||||
; This is the list of fields to include in the record. The field names are the
|
||||
; same as in the legacy mapping but without any enclosing dialplan functions.
|
||||
; You can specify literals to be placed in the output record by double-quoting
|
||||
; them. There is also some special notation available in the form of "qualifiers".
|
||||
; A qualifier is a list of tags, separated by the '^' character and placed
|
||||
; directly after the field name and enclosed in parentheses.
|
||||
;
|
||||
; If you've added user variables to the CDR using the CDR() dialplan function in
|
||||
; extensions.conf, you'll need to retrieve the field using the "uservar" qualifier.
|
||||
; For example, if you've added the "ds_type" variable, you'll need to retrieve
|
||||
; it with `ds_type(uservar)`.
|
||||
;
|
||||
; The default output format for the "start", "answer" and "end" timestamp fields
|
||||
; is the "%Y-%m-%d %T" strftime string format however you can also format those
|
||||
; fields as an int64 or a float: `start(int64),answer(float),end`.
|
||||
;
|
||||
; The "disposition" and "amaflags" are formatted as their string names like
|
||||
; "ANSWERED" and "DOCUMENTATION" by default but if you just want the numbers and
|
||||
; not the names... `amaflags(int64),disposition(int64)`.
|
||||
;
|
||||
; If you need to combine flags, use the caret '^' symbol: `start(int64^noquote)`
|
||||
;
|
||||
; Final notes about "fields":
|
||||
; Field names and qualifiers aren't case sensitive.
|
||||
; You MUST use commas to separate the fields.
|
||||
; You MUST use double-quotes to indicate literal fields.
|
||||
; Whitespace in "fields" is ignored except in literals.
|
||||
|
||||
;busy_timeout = 1000 ; The number of milliseconds to wait for a database operation
|
||||
; to complete before an error is returned.
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 Protected header for the CDR and CEL Custom Backends
|
||||
*
|
||||
* \warning This file should be included only by CDR and CEL backends.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _RES_CDREL_CUSTOM_H
|
||||
#define _RES_CDREL_CUSTOM_H
|
||||
|
||||
/*! \enum Backend Types */
|
||||
enum cdrel_backend_type {
|
||||
cdrel_backend_text = 0, /*!< Text file: DSV or JSON */
|
||||
cdrel_backend_db, /*!< Database (currently only sqlite3) */
|
||||
cdrel_backend_type_end, /*!< Sentinel */
|
||||
};
|
||||
|
||||
/*! \enum Record Types */
|
||||
enum cdrel_record_type {
|
||||
cdrel_record_cdr = 0, /*!< Call Detail Records */
|
||||
cdrel_record_cel, /*!< Channel Event Log records */
|
||||
cdrel_record_type_end, /*!< Sentinel */
|
||||
};
|
||||
|
||||
/*! \struct Forward declaration of a configuration */
|
||||
struct cdrel_config;
|
||||
|
||||
/*! \struct Vector to hold all configurations in a config file */
|
||||
AST_VECTOR(cdrel_configs, struct cdrel_config *);
|
||||
|
||||
/*!
|
||||
* \brief Perform initial module load.
|
||||
*
|
||||
* Needs to be called by each "custom" module
|
||||
*
|
||||
* \param backend_type One of \ref cdrel_backend_type.
|
||||
* \param record_type One of \ref cdrel_record_type.
|
||||
* \param config_filename The config file name.
|
||||
* \param backend_name The name to register the backend as.
|
||||
* \param logging_cb The logging callback to register with CDR or CEL.
|
||||
* \returns A pointer to a VECTOR or config objects read from the config file.
|
||||
*/
|
||||
struct cdrel_configs *cdrel_load_module(enum cdrel_backend_type backend_type,
|
||||
enum cdrel_record_type record_type, const char *config_filename,
|
||||
const char *backend_name, void *logging_cb);
|
||||
|
||||
/*!
|
||||
* \brief Perform module reload.
|
||||
*
|
||||
* Needs to be called by each "custom" module
|
||||
*
|
||||
* \warning This function MUST be called with the module's config_lock held
|
||||
* for writing to prevent reloads from happening while we're logging.
|
||||
*
|
||||
* \param backend_type One of \ref cdrel_backend_type.
|
||||
* \param record_type One of \ref cdrel_record_type.
|
||||
* \param configs A pointer to the VECTOR of config objects returned by \ref cdrel_load_module.
|
||||
* \param config_filename The config file name.
|
||||
* \retval AST_MODULE_LOAD_SUCCESS
|
||||
* \retval AST_MODULE_LOAD_DECLINE
|
||||
*/
|
||||
int cdrel_reload_module(enum cdrel_backend_type backend_type, enum cdrel_record_type record_type,
|
||||
struct cdrel_configs **configs, const char *config_filename);
|
||||
|
||||
/*!
|
||||
* \brief Perform module unload.
|
||||
*
|
||||
* Needs to be called by each "custom" module
|
||||
*
|
||||
* \warning This function MUST be called with the module's config_lock held
|
||||
* for writing to prevent the module from being unloaded while we're logging.
|
||||
*
|
||||
* \param backend_type One of \ref cdrel_backend_type.
|
||||
* \param record_type One of \ref cdrel_record_type.
|
||||
* \param configs A pointer to the VECTOR of config objects returned by \ref cdrel_load_module.
|
||||
* \param backend_name The backend name to unregister.
|
||||
* \retval 0 Success.
|
||||
* \retval -1 Failure.
|
||||
*/
|
||||
int cdrel_unload_module(enum cdrel_backend_type backend_type, enum cdrel_record_type record_type,
|
||||
struct cdrel_configs *configs, const char *backend_name);
|
||||
|
||||
/*!
|
||||
* \brief Log a record. The module's \ref logging_cb must call this.
|
||||
*
|
||||
* \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 A pointer to the VECTOR of config objects returned by \ref cdrel_load_module.
|
||||
* \param data A pointer to an ast_cdr or ast_event object to log.
|
||||
* \retval 0 Success.
|
||||
* \retval -1 Failure.
|
||||
*/
|
||||
int cdrel_logger(struct cdrel_configs *configs, void *data);
|
||||
|
||||
#endif /* _RES_CDREL_CUSTOM_H */
|
||||
@ -0,0 +1,363 @@
|
||||
/*
|
||||
* 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 Private header for res_cdrel_custom.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _CDREL_H
|
||||
#define _CDREL_H
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "asterisk.h"
|
||||
#include "asterisk/cdr.h"
|
||||
#include "asterisk/cel.h"
|
||||
#include "asterisk/event.h"
|
||||
#include "asterisk/lock.h"
|
||||
#include "asterisk/strings.h"
|
||||
#include "asterisk/vector.h"
|
||||
#include "asterisk/res_cdrel_custom.h"
|
||||
|
||||
extern const char *cdrel_record_type_map[];
|
||||
#define RECORD_TYPE_STR(_rt) (cdrel_record_type_map[_rt])
|
||||
|
||||
extern const char *cdrel_module_type_map[];
|
||||
#define MODULE_TYPE_STR(_mt) (cdrel_module_type_map[_mt])
|
||||
|
||||
enum cdrel_text_format_type {
|
||||
cdrel_format_dsv = 0,
|
||||
cdrel_format_json,
|
||||
cdrel_format_sql,
|
||||
cdrel_format_type_end,
|
||||
};
|
||||
|
||||
enum cdrel_config_type {
|
||||
cdrel_config_legacy = 0,
|
||||
cdrel_config_advanced,
|
||||
cdrel_config_type_end,
|
||||
};
|
||||
|
||||
enum cdrel_quoting_method {
|
||||
cdrel_quoting_method_none = 0,
|
||||
cdrel_quoting_method_all,
|
||||
cdrel_quoting_method_minimal,
|
||||
cdrel_quoting_method_non_numeric,
|
||||
cdrel_quoting_method_end,
|
||||
};
|
||||
|
||||
/*
|
||||
* ORDER IS IMPORTANT!
|
||||
* The string output data types need to be first.
|
||||
*/
|
||||
enum cdrel_data_type {
|
||||
cdrel_type_string = 0,
|
||||
cdrel_type_timeval,
|
||||
cdrel_type_literal,
|
||||
cdrel_type_amaflags,
|
||||
cdrel_type_disposition,
|
||||
cdrel_type_uservar,
|
||||
cdrel_type_event_type,
|
||||
cdrel_type_event_enum,
|
||||
cdrel_data_type_strings_end,
|
||||
cdrel_type_int32,
|
||||
cdrel_type_uint32,
|
||||
cdrel_type_int64,
|
||||
cdrel_type_uint64,
|
||||
cdrel_type_float,
|
||||
cdrel_data_type_end
|
||||
};
|
||||
|
||||
extern const char *cdrel_data_type_map[];
|
||||
#define DATA_TYPE_STR(_dt) (_dt < cdrel_data_type_end ? cdrel_data_type_map[_dt] : NULL)
|
||||
enum cdrel_data_type cdrel_data_type_from_str(const char *str);
|
||||
|
||||
#define CDREL_FIELD_FLAG_QUOTE (0)
|
||||
#define CDREL_FIELD_FLAG_NOQUOTE (1)
|
||||
#define CDREL_FIELD_FLAG_TYPE_FORCED (2)
|
||||
#define CDREL_FIELD_FLAG_USERVAR (3)
|
||||
#define CDREL_FIELD_FLAG_LITERAL (4)
|
||||
#define CDREL_FIELD_FLAG_FORMAT_SPEC (5)
|
||||
#define CDREL_FIELD_FLAG_LAST (6)
|
||||
|
||||
enum cdrel_field_flags {
|
||||
cdrel_flag_quote = (1 << CDREL_FIELD_FLAG_QUOTE),
|
||||
cdrel_flag_noquote = (1 << CDREL_FIELD_FLAG_NOQUOTE),
|
||||
cdrel_flag_type_forced = (1 << CDREL_FIELD_FLAG_TYPE_FORCED),
|
||||
cdrel_flag_uservar = (1 << CDREL_FIELD_FLAG_USERVAR),
|
||||
cdrel_flag_literal = (1 << CDREL_FIELD_FLAG_LITERAL),
|
||||
cdrel_flag_format_spec = (1 << CDREL_FIELD_FLAG_FORMAT_SPEC),
|
||||
cdrel_field_flags_end = (1 << CDREL_FIELD_FLAG_LAST),
|
||||
};
|
||||
|
||||
/*
|
||||
* CEL has a few synthetic fields that aren't defined
|
||||
* in event.h so we'll define them ourselves after the
|
||||
* last official id.
|
||||
*/
|
||||
#define AST_EVENT_IE_CEL_LITERAL (AST_EVENT_IE_TOTAL + 1)
|
||||
#define AST_EVENT_IE_CEL_EVENT_ENUM (AST_EVENT_IE_TOTAL + 2)
|
||||
#define LAST_CEL_ID AST_EVENT_IE_CEL_EVENT_ENUM
|
||||
|
||||
/*!
|
||||
* While CEL event fields are published as a generic AST_EVENT with
|
||||
* a field id assigned to each field, CDRs are published as a fixed
|
||||
* ast_cdr structure. To make it easier to share lower level code,
|
||||
* we assign pseudo-field-ids to each CDR field that are really the offset
|
||||
* of the field in the structure. This allows us to generically get
|
||||
* any field using its id just like we do for CEL.
|
||||
*
|
||||
* To avoid conflicts with the existing CEL field ids, we'll start these
|
||||
* after the last one.
|
||||
*/
|
||||
#define CDR_OFFSET_SHIFT (LAST_CEL_ID + 1)
|
||||
#define CDR_OFFSETOF(_field) (offsetof(struct ast_cdr, _field) + CDR_OFFSET_SHIFT)
|
||||
#define CDR_FIELD(__cdr, __offset) (((void *)__cdr) + __offset - CDR_OFFSET_SHIFT)
|
||||
|
||||
enum cdr_field_id {
|
||||
cdr_field_literal = -1,
|
||||
cdr_field_clid = CDR_OFFSETOF(clid),
|
||||
cdr_field_src = CDR_OFFSETOF(src),
|
||||
cdr_field_dst = CDR_OFFSETOF(dst),
|
||||
cdr_field_dcontext = CDR_OFFSETOF(dcontext),
|
||||
cdr_field_channel = CDR_OFFSETOF(channel),
|
||||
cdr_field_dstchannel = CDR_OFFSETOF(dstchannel),
|
||||
cdr_field_lastapp = CDR_OFFSETOF(lastapp),
|
||||
cdr_field_lastdata = CDR_OFFSETOF(lastdata),
|
||||
cdr_field_start = CDR_OFFSETOF(start),
|
||||
cdr_field_answer = CDR_OFFSETOF(answer),
|
||||
cdr_field_end = CDR_OFFSETOF(end),
|
||||
cdr_field_duration = CDR_OFFSETOF(duration),
|
||||
cdr_field_billsec = CDR_OFFSETOF(billsec),
|
||||
cdr_field_disposition = CDR_OFFSETOF(disposition),
|
||||
cdr_field_amaflags = CDR_OFFSETOF(amaflags),
|
||||
cdr_field_accountcode = CDR_OFFSETOF(accountcode),
|
||||
cdr_field_peeraccount = CDR_OFFSETOF(peeraccount),
|
||||
cdr_field_flags = CDR_OFFSETOF(flags),
|
||||
cdr_field_uniqueid = CDR_OFFSETOF(uniqueid),
|
||||
cdr_field_linkedid = CDR_OFFSETOF(linkedid),
|
||||
cdr_field_tenantid = CDR_OFFSETOF(tenantid),
|
||||
cdr_field_peertenantid = CDR_OFFSETOF(peertenantid),
|
||||
cdr_field_userfield = CDR_OFFSETOF(userfield),
|
||||
cdr_field_sequence = CDR_OFFSETOF(sequence),
|
||||
cdr_field_varshead = CDR_OFFSETOF(varshead),
|
||||
};
|
||||
|
||||
|
||||
struct cdrel_field;
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief A generic value wrapper structure.
|
||||
*/
|
||||
struct cdrel_value {
|
||||
char *field_name;
|
||||
enum cdrel_data_type data_type;
|
||||
int mallocd;
|
||||
union {
|
||||
char *string;
|
||||
int32_t int32;
|
||||
uint32_t uint32;
|
||||
int64_t int64;
|
||||
uint64_t uint64;
|
||||
struct timeval tv;
|
||||
float floater;
|
||||
} values;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief A vector of cdrel_values.
|
||||
*/
|
||||
AST_VECTOR(cdrel_values, struct cdrel_value *);
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Getter callbacks.
|
||||
*
|
||||
* \param record An ast_cdr or ast_event structure.
|
||||
* \param config Config object.
|
||||
* \param field Field object.
|
||||
* \param value A pointer to a cdrel_value structure to populate.
|
||||
* \retval 0 on success.
|
||||
* \retval -1 on failure.
|
||||
*/
|
||||
typedef int (*cdrel_field_getter)(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value);
|
||||
/*!
|
||||
* \internal
|
||||
* \brief The table of getter callbacks. Populated by getters_cdr.c and getters_cel.c.
|
||||
*
|
||||
* Defined in res_cdrel_custom.c
|
||||
*/
|
||||
extern cdrel_field_getter cdrel_field_getters[cdrel_record_type_end][cdrel_data_type_end];
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Data type formatters.
|
||||
*
|
||||
* \param config Config object.
|
||||
* \param field Field object.
|
||||
* \param input_value A pointer to a cdrel_value structure with the data to format.
|
||||
* \param output_value A pointer to a cdrel_value structure to receive the formatted data.
|
||||
* \retval 0 on success.
|
||||
* \retval -1 on failure.
|
||||
*/
|
||||
typedef int (*cdrel_field_formatter)(struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *input_value, struct cdrel_value *output_value);
|
||||
/*!
|
||||
* \internal
|
||||
* \brief The table of formatter callbacks. Populated by formatters.c.
|
||||
*
|
||||
* Defined in res_cdrel_custom.c
|
||||
*/
|
||||
extern cdrel_field_formatter cdrel_field_formatters[cdrel_data_type_end];
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Backend writers.
|
||||
*
|
||||
* \param config Config object.
|
||||
* \param values A vector of cdrel_values to write.
|
||||
* \retval 0 on success.
|
||||
* \retval -1 on failure.
|
||||
*/
|
||||
typedef int (*cdrel_backend_writer)(struct cdrel_config *config, struct cdrel_values *values);
|
||||
/*!
|
||||
* \internal
|
||||
* \brief The table of writer callbacks. Populated by writers.c.
|
||||
*
|
||||
* Defined in res_cdrel_custom.c
|
||||
*/
|
||||
extern cdrel_backend_writer cdrel_backend_writers[cdrel_format_type_end];
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Dummy channel allocators.
|
||||
*
|
||||
* Legacy configurations use dialplan functions so they need a dummy channel
|
||||
* to operate on. CDR and CEL each have their own allocator.
|
||||
*
|
||||
* \param config Config object.
|
||||
* \param record An ast_cdr or ast_event structure.
|
||||
* \return an ast_channel or NULL on failure.
|
||||
*/
|
||||
typedef struct ast_channel * (*cdrel_dummy_channel_alloc)(struct cdrel_config *config, void *record);
|
||||
extern cdrel_dummy_channel_alloc cdrel_dummy_channel_allocators[cdrel_format_type_end];
|
||||
|
||||
|
||||
/*! Represents a field definition */
|
||||
struct cdrel_field {
|
||||
enum cdrel_record_type record_type; /*!< CDR or CEL */
|
||||
int field_id; /*!< May be an AST_EVENT_IE_CEL or a cdr_field_id */
|
||||
char *name; /*!< The name of the field */
|
||||
enum cdrel_data_type input_data_type; /*!< The data type of the field in the source record */
|
||||
enum cdrel_data_type output_data_type;
|
||||
struct ast_flags flags; /*!< Flags used during config parsing */
|
||||
char data[1]; /*!< Could be literal data, a user variable name, etc */
|
||||
};
|
||||
|
||||
/*! Represents an output definition from a config file */
|
||||
struct cdrel_config {
|
||||
enum cdrel_record_type record_type; /*!< CDR or CEL */
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
AST_STRING_FIELD(config_filename); /*!< Input configuration filename */
|
||||
AST_STRING_FIELD(output_filename); /*!< Output text file or database */
|
||||
AST_STRING_FIELD(template); /*!< Input template */
|
||||
AST_STRING_FIELD(db_columns); /*!< List of columns for database backends */
|
||||
AST_STRING_FIELD(db_table); /*!< Table name for database backends */
|
||||
);
|
||||
sqlite3 *db; /*!< sqlite3 database handle */
|
||||
sqlite3_stmt *insert; /*!< sqlite3 prepared statement for insert */
|
||||
int busy_timeout; /*!< sqlite3 query timeout value */
|
||||
cdrel_dummy_channel_alloc dummy_channel_alloc; /*!< Legacy config types need a dummy channel */
|
||||
enum cdrel_backend_type backend_type; /*!< Text file or database */
|
||||
enum cdrel_config_type config_type; /*!< Legacy or advanced */
|
||||
enum cdrel_text_format_type format_type; /*!< For text files, CSV or JSON */
|
||||
enum cdrel_quoting_method quoting_method; /*!< When to quote */
|
||||
char separator[2]; /*!< For text files, the field separator */
|
||||
char quote[2]; /*!< For text files, the quote character */
|
||||
char quote_escape[2]; /*!< For text files, character to use to escape embedded quotes */
|
||||
AST_VECTOR(, struct cdrel_field *) fields; /*!< Vector of fields for this config */
|
||||
ast_mutex_t lock; /*!< Lock that serializes filesystem writes */
|
||||
};
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Get a cdrel_field structure by record type and field name.
|
||||
*
|
||||
* \param record_type The cdrel_record_type to search.
|
||||
* \param name The field name to search for.
|
||||
* \returns A pointer to a constant cdrel_field structure or NULL if not found.
|
||||
* This pointer must never be freed.
|
||||
*/
|
||||
const struct cdrel_field *get_registered_field_by_name(enum cdrel_record_type record_type,
|
||||
const char *name);
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Write a record to a text file
|
||||
*
|
||||
* \param config The configuration object.
|
||||
* \param record The data to write.
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
int write_record_to_file(struct cdrel_config *config, struct ast_str *record);
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Write a record to a database
|
||||
*
|
||||
* \param config The configuration object.
|
||||
* \param values The values to write.
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
int write_record_to_database(struct cdrel_config *config, struct cdrel_values *values);
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Return the basename of a path
|
||||
*
|
||||
* \param path
|
||||
* \returns Pointer to last '/' or path if no '/' was found.
|
||||
*/
|
||||
const char *cdrel_basename(const char *path);
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Get a string representing a field's flags
|
||||
*
|
||||
* \param flags An ast_flags structure
|
||||
* \param str Pointer to ast_str* buffer
|
||||
* \returns A string of field flag names
|
||||
*/
|
||||
const char *cdrel_get_field_flags(struct ast_flags *flags, struct ast_str **str);
|
||||
|
||||
|
||||
int load_cdr(void);
|
||||
int load_cel(void);
|
||||
int load_formatters(void);
|
||||
int load_writers(void);
|
||||
|
||||
#endif /* _CDREL_H */
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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 Formatters
|
||||
*
|
||||
*/
|
||||
|
||||
#include "cdrel.h"
|
||||
#include "asterisk/json.h"
|
||||
#include "asterisk/cdr.h"
|
||||
#include "asterisk/cel.h"
|
||||
|
||||
static char *quote_escaper(const char *value, char quote, char quote_escape, char *qvalue)
|
||||
{
|
||||
char *ptr = qvalue;
|
||||
const char *dataptr = value;
|
||||
|
||||
if (!qvalue) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
while(*dataptr != '\0') {
|
||||
if (*dataptr == quote) {
|
||||
*ptr++ = quote_escape;
|
||||
}
|
||||
*ptr++ = *dataptr++;
|
||||
}
|
||||
*ptr='\0';
|
||||
return qvalue;
|
||||
}
|
||||
|
||||
static int format_string(struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *input_value, struct cdrel_value *output_value)
|
||||
{
|
||||
int quotes_count = 0;
|
||||
int needs_quoting = ast_test_flag(&field->flags, cdrel_flag_quote);
|
||||
int ix = 0;
|
||||
int input_len = strlen(input_value->values.string ?: "");
|
||||
int res = 0;
|
||||
char *qvalue = NULL;
|
||||
char *evalue = (char *)input_value->values.string;
|
||||
|
||||
output_value->data_type = cdrel_type_string;
|
||||
output_value->field_name = input_value->field_name;
|
||||
|
||||
if (input_len == 0) {
|
||||
output_value->values.string = ast_strdup(needs_quoting ? "\"\"" : "");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (ix = 0; ix < input_len; ix++) {
|
||||
char c = input_value->values.string[ix];
|
||||
if (c == config->quote[0]) {
|
||||
quotes_count++;
|
||||
needs_quoting = 1;
|
||||
} else if (c == config->separator[0] || c == '\r' || c == '\n') {
|
||||
needs_quoting = 1;
|
||||
}
|
||||
}
|
||||
|
||||
ast_debug(5, "%s: %s=%s %s", cdrel_basename(config->output_filename), input_value->field_name,
|
||||
input_value->values.string, ast_str_tmp(128, cdrel_get_field_flags(&field->flags, &STR_TMP)));
|
||||
|
||||
if (!needs_quoting) {
|
||||
output_value->values.string = ast_strdup(input_value->values.string);
|
||||
return output_value->values.string == NULL ? -1 : 0;
|
||||
}
|
||||
|
||||
/* For every quote_count, we need an extra byte for the quote escape character. */
|
||||
qvalue = ast_alloca(input_len + quotes_count + 1);
|
||||
if (quotes_count) {
|
||||
evalue = quote_escaper(input_value->values.string, config->quote[0], config->quote_escape[0], qvalue);
|
||||
}
|
||||
res = ast_asprintf(&output_value->values.string, "%s%s%s", config->quote, evalue, config->quote);
|
||||
return res < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
#define DEFINE_FORMATTER(_name, _field, _type, _fmt) \
|
||||
static int format_ ## _name (struct cdrel_config *config, \
|
||||
struct cdrel_field *field, struct cdrel_value *input_value, struct cdrel_value *output_value) \
|
||||
{ \
|
||||
int res = 0; \
|
||||
char *quote = ""; \
|
||||
if (input_value->data_type != output_value->data_type) { \
|
||||
/* Forward to other formatter */ \
|
||||
return cdrel_field_formatters[output_value->data_type](config, field, input_value, output_value); \
|
||||
} \
|
||||
output_value->field_name = input_value->field_name; \
|
||||
if ((config->quoting_method == cdrel_quoting_method_all || ast_test_flag(&field->flags, cdrel_flag_quote)) \
|
||||
&& !ast_test_flag(&field->flags, cdrel_flag_noquote)) { \
|
||||
quote = config->quote; \
|
||||
} \
|
||||
res = ast_asprintf(&output_value->values.string, "%s" _fmt "%s", quote, input_value->values._field, quote); \
|
||||
output_value->data_type = cdrel_type_string; \
|
||||
return res < 0 ? -1 : 0; \
|
||||
}
|
||||
|
||||
DEFINE_FORMATTER(uint32, uint32, uint32_t, "%u")
|
||||
DEFINE_FORMATTER(int32, int32, int32_t, "%d")
|
||||
DEFINE_FORMATTER(uint64, uint64, uint64_t, "%lu")
|
||||
DEFINE_FORMATTER(int64, int64, int64_t, "%ld")
|
||||
DEFINE_FORMATTER(float, floater, float, "%.1f")
|
||||
|
||||
static int format_timeval(struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *input_value, struct cdrel_value *output_value)
|
||||
{
|
||||
struct ast_tm tm;
|
||||
char tempbuf[64] = { 0, };
|
||||
int res = 0;
|
||||
const char *format = "%Y-%m-%d %T";
|
||||
|
||||
output_value->field_name = input_value->field_name;
|
||||
|
||||
if (field->output_data_type == cdrel_type_int64) {
|
||||
output_value->data_type = cdrel_type_int64;
|
||||
output_value->values.int64 = input_value->values.tv.tv_sec;
|
||||
return format_int64(config, field, output_value, output_value);
|
||||
} else if (field->output_data_type == cdrel_type_float) {
|
||||
output_value->data_type = cdrel_type_float;
|
||||
output_value->values.floater = ((float)input_value->values.tv.tv_sec) + ((float)input_value->values.tv.tv_usec) / 1000000.0;
|
||||
return format_float(config, field, output_value, output_value);
|
||||
} else if (!ast_strlen_zero(field->data)) {
|
||||
format = field->data;
|
||||
}
|
||||
|
||||
if (input_value->values.tv.tv_sec > 0) {
|
||||
ast_localtime(&input_value->values.tv, &tm, NULL);
|
||||
ast_strftime(tempbuf, sizeof(tempbuf), format, &tm);
|
||||
}
|
||||
input_value->values.string = tempbuf;
|
||||
input_value->data_type = cdrel_type_string;
|
||||
output_value->data_type = cdrel_type_string;
|
||||
res = format_string(config, field, input_value, output_value);
|
||||
return res;
|
||||
}
|
||||
|
||||
static int format_amaflags(struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *input_value, struct cdrel_value *output_value)
|
||||
{
|
||||
int res = 0;
|
||||
|
||||
input_value->values.string = (char *)ast_channel_amaflags2string(input_value->values.int64);
|
||||
input_value->data_type = cdrel_type_string;
|
||||
output_value->data_type = cdrel_type_string;
|
||||
res = format_string(config, field, input_value, output_value);
|
||||
return res;
|
||||
}
|
||||
|
||||
static int format_disposition(struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *input_value, struct cdrel_value *output_value)
|
||||
{
|
||||
int res = 0;
|
||||
|
||||
input_value->values.string = (char *)ast_cdr_disp2str(input_value->values.int64);
|
||||
input_value->data_type = cdrel_type_string;
|
||||
output_value->data_type = cdrel_type_string;
|
||||
res = format_string(config, field, input_value, output_value);
|
||||
return res;
|
||||
}
|
||||
|
||||
int load_formatters(void)
|
||||
{
|
||||
ast_debug(1, "Loading Formatters\n");
|
||||
cdrel_field_formatters[cdrel_type_string] = format_string;
|
||||
cdrel_field_formatters[cdrel_type_int32] = format_int32;
|
||||
cdrel_field_formatters[cdrel_type_uint32] = format_uint32;
|
||||
cdrel_field_formatters[cdrel_type_int64] = format_int64;
|
||||
cdrel_field_formatters[cdrel_type_uint64] = format_uint64;
|
||||
cdrel_field_formatters[cdrel_type_timeval] = format_timeval;
|
||||
cdrel_field_formatters[cdrel_type_float] = format_float;
|
||||
cdrel_field_formatters[cdrel_type_amaflags] = format_amaflags;
|
||||
cdrel_field_formatters[cdrel_type_disposition] = format_disposition;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 CDR Getters
|
||||
*
|
||||
*/
|
||||
|
||||
#include "cdrel.h"
|
||||
#include "asterisk/cdr.h"
|
||||
|
||||
static int cdr_get_string(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_cdr *cdr = record;
|
||||
value->data_type = field->input_data_type;
|
||||
value->field_name = field->name;
|
||||
value->values.string = CDR_FIELD(cdr, field->field_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define DEFINE_CDR_GETTER(_sname, _ename, _type) \
|
||||
static int cdr_get_ ## _ename (void *record, struct cdrel_config *config, \
|
||||
struct cdrel_field *field, struct cdrel_value *value) \
|
||||
{ \
|
||||
struct ast_cdr *cdr = record; \
|
||||
value->data_type = field->input_data_type; \
|
||||
value->field_name = field->name; \
|
||||
value->values._sname = *(_type *)CDR_FIELD(cdr, field->field_id); \
|
||||
return 0; \
|
||||
}\
|
||||
|
||||
DEFINE_CDR_GETTER(int32, int32, int32_t)
|
||||
DEFINE_CDR_GETTER(uint32, uint32, uint32_t)
|
||||
DEFINE_CDR_GETTER(int64, int64, int64_t)
|
||||
DEFINE_CDR_GETTER(uint64, uint64, uint64_t)
|
||||
DEFINE_CDR_GETTER(tv, timeval, struct timeval)
|
||||
DEFINE_CDR_GETTER(floater, float, float)
|
||||
|
||||
static int cdr_get_literal(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
value->data_type = cdrel_type_string;
|
||||
value->field_name = field->name;
|
||||
value->values.string = field->data;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cdr_get_uservar(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_cdr *cdr = record;
|
||||
struct ast_var_t *variables;
|
||||
const char *rtn = NULL;
|
||||
|
||||
value->data_type = cdrel_type_string;
|
||||
value->field_name = field->name;
|
||||
AST_LIST_TRAVERSE(&cdr->varshead, variables, entries) {
|
||||
if (strcasecmp(field->data, ast_var_name(variables)) == 0) {
|
||||
rtn = ast_var_value(variables);
|
||||
}
|
||||
}
|
||||
|
||||
value->values.string = (char *)S_OR(rtn, "");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct ast_channel *dummy_chan_alloc_cdr(struct cdrel_config *config, void *data)
|
||||
{
|
||||
struct ast_cdr *cdr = data;
|
||||
struct ast_channel *dummy = NULL;
|
||||
|
||||
dummy = ast_dummy_channel_alloc();
|
||||
if (!dummy) {
|
||||
ast_log(LOG_ERROR, "Unable to fabricate channel from CDR for '%s'\n",
|
||||
config->output_filename);
|
||||
return NULL;
|
||||
}
|
||||
ast_channel_cdr_set(dummy, ast_cdr_dup(cdr));
|
||||
return dummy;
|
||||
}
|
||||
|
||||
int load_cdr(void)
|
||||
{
|
||||
ast_debug(1, "Loading CDR getters\n");
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_string] = cdr_get_string;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_literal] = cdr_get_literal;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_int32] = cdr_get_int32;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_uint32] = cdr_get_uint32;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_int64] = cdr_get_int64;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_uint64] = cdr_get_uint64;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_timeval] = cdr_get_timeval;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_float] = cdr_get_float;
|
||||
cdrel_field_getters[cdrel_record_cdr][cdrel_type_uservar] = cdr_get_uservar;
|
||||
cdrel_dummy_channel_allocators[cdrel_record_cdr] = dummy_chan_alloc_cdr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 CEL Getters
|
||||
*
|
||||
*/
|
||||
|
||||
#include "cdrel.h"
|
||||
#include "asterisk/cel.h"
|
||||
|
||||
static int cel_get_string(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_event *event = record;
|
||||
value->data_type = cdrel_type_string;
|
||||
value->field_name = field->name;
|
||||
value->values.string = (char *)ast_event_get_ie_str(event, field->field_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cel_get_literal(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
value->data_type = cdrel_type_string;
|
||||
value->field_name = field->name;
|
||||
value->values.string = field->data;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cel_get_timeval(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_event *event = record;
|
||||
value->data_type = cdrel_type_timeval;
|
||||
value->field_name = field->name;
|
||||
value->values.tv.tv_sec = ast_event_get_ie_uint(event, AST_EVENT_IE_CEL_EVENT_TIME);
|
||||
value->values.tv.tv_usec = ast_event_get_ie_uint(event, AST_EVENT_IE_CEL_EVENT_TIME_USEC);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cel_get_uint32(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_event *event = record;
|
||||
value->data_type = cdrel_type_uint32;
|
||||
value->field_name = field->name;
|
||||
value->values.uint32 = ast_event_get_ie_uint(event, field->field_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cel_get_event_type(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_event *event = record;
|
||||
const char *val = NULL;
|
||||
value->data_type = cdrel_type_string;
|
||||
value->field_name = field->name;
|
||||
|
||||
if (ast_event_get_ie_uint(event, AST_EVENT_IE_CEL_EVENT_TYPE) == AST_CEL_USER_DEFINED) {
|
||||
val = ast_event_get_ie_str(event, AST_EVENT_IE_CEL_USEREVENT_NAME);
|
||||
} else {
|
||||
val = ast_cel_get_type_name(ast_event_get_ie_uint(event, AST_EVENT_IE_CEL_EVENT_TYPE));
|
||||
}
|
||||
value->values.string = (char *)val;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cel_get_event_enum(void *record, struct cdrel_config *config,
|
||||
struct cdrel_field *field, struct cdrel_value *value)
|
||||
{
|
||||
struct ast_event *event = record;
|
||||
value->data_type = cdrel_type_string;
|
||||
value->field_name = field->name;
|
||||
value->values.string = (char *)ast_cel_get_type_name(ast_event_get_ie_uint(event, AST_EVENT_IE_CEL_EVENT_TYPE));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct ast_channel *dummy_chan_alloc_cel(struct cdrel_config *config, void *data)
|
||||
{
|
||||
struct ast_event *event = data;
|
||||
|
||||
return ast_cel_fabricate_channel_from_event(event);
|
||||
}
|
||||
|
||||
int load_cel(void)
|
||||
{
|
||||
ast_debug(1, "Loading CEL getters\n");
|
||||
cdrel_field_getters[cdrel_record_cel][cdrel_type_string] = cel_get_string;
|
||||
cdrel_field_getters[cdrel_record_cel][cdrel_type_literal] = cel_get_literal;
|
||||
cdrel_field_getters[cdrel_record_cel][cdrel_type_uint32] = cel_get_uint32;
|
||||
cdrel_field_getters[cdrel_record_cel][cdrel_type_timeval] = cel_get_timeval;
|
||||
cdrel_field_getters[cdrel_record_cel][cdrel_type_event_type] = cel_get_event_type;
|
||||
cdrel_field_getters[cdrel_record_cel][cdrel_type_event_enum] = cel_get_event_enum;
|
||||
cdrel_dummy_channel_allocators[cdrel_record_cel] = dummy_chan_alloc_cel;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 CDR/CEL field registry
|
||||
*
|
||||
*/
|
||||
|
||||
#include "cdrel.h"
|
||||
|
||||
#include "asterisk/json.h"
|
||||
#include "asterisk/cdr.h"
|
||||
#include "asterisk/cel.h"
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Helper macro that populates the static array of cdrel_fields.
|
||||
*
|
||||
* \param _record_type The type of record: CDR or CEL.
|
||||
* \param _field_id For CEL, it's the event field. For CDR it's one of cdr_field_id.
|
||||
* \param _name The field name.
|
||||
* \param _input_type The input data type. Drives the getters.
|
||||
* \param output_types An array of types, one each for dsv, json and sql. Drives the formatters.
|
||||
* \param _mallocd Not used.
|
||||
*/
|
||||
#define REGISTER_FIELD(_record_type, _field_id, _name, _input_type, _output_type) \
|
||||
{ _record_type, _field_id, _name, _input_type, _output_type, { 0 } }
|
||||
|
||||
static const struct cdrel_field cdrel_field_registry[] = {
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_EVENT_ENUM, "eventenum", cdrel_type_event_enum, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_EVENT_TYPE, "eventtype", cdrel_type_event_type, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_EVENT_TIME, "eventtime", cdrel_type_timeval, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_EVENT_TIME_USEC, "eventtimeusec", cdrel_type_uint32, cdrel_type_uint32),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_USEREVENT_NAME, "usereventname", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_USEREVENT_NAME, "userdeftype", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CIDNAME, "name", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CIDNUM, "num", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_EXTEN, "exten", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CONTEXT, "context", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CHANNAME, "channame", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_APPNAME, "appname", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_APPDATA, "appdata", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_AMAFLAGS, "amaflags", cdrel_type_uint32, cdrel_type_uint32),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_ACCTCODE, "accountcode", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_UNIQUEID, "uniqueid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_USERFIELD, "userfield", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CIDANI, "ani", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CIDRDNIS, "rdnis", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_CIDDNID, "dnid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_PEER, "peer", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_PEER, "bridgepeer", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_LINKEDID, "linkedid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_PEERACCT, "peeraccount", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_EXTRA, "eventextra", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_TENANTID, "tenantid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cel, AST_EVENT_IE_CEL_LITERAL, "literal", cdrel_type_literal, cdrel_type_string),
|
||||
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_clid, "clid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_src, "src", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_dst, "dst", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_dcontext, "dcontext", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_channel, "channel", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_dstchannel, "dstchannel", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_lastapp, "lastapp", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_lastdata, "lastdata", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_start, "start", cdrel_type_timeval, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_answer, "answer", cdrel_type_timeval, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_end, "end", cdrel_type_timeval, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_duration, "duration", cdrel_type_int64, cdrel_type_int64),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_billsec, "billsec", cdrel_type_int64, cdrel_type_int64),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_disposition, "disposition", cdrel_type_int64, cdrel_type_disposition),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_amaflags, "amaflags", cdrel_type_int64, cdrel_type_amaflags),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_accountcode, "accountcode", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_peeraccount, "peeraccount", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_flags, "flags", cdrel_type_uint32, cdrel_type_uint32),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_uniqueid, "uniqueid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_linkedid, "linkedid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_tenantid, "tenantid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_peertenantid, "peertenantid", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_userfield, "userfield", cdrel_type_string, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_sequence, "sequence", cdrel_type_int32, cdrel_type_int32),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_varshead, "uservar", cdrel_type_uservar, cdrel_type_string),
|
||||
REGISTER_FIELD(cdrel_record_cdr, cdr_field_literal, "literal", cdrel_type_literal, cdrel_type_string),
|
||||
};
|
||||
|
||||
/*
|
||||
* \internal
|
||||
* \brief Get a cdrel_field structure by record type and field name.
|
||||
*
|
||||
* \param record_type The cdrel_record_type to search.
|
||||
* \param name The field name to search for.
|
||||
* \returns A pointer to a constant cdrel_field structure or NULL if not found.
|
||||
* This pointer must never be freed.
|
||||
*/
|
||||
const struct cdrel_field *get_registered_field_by_name(enum cdrel_record_type type, const char *name)
|
||||
{
|
||||
int ix = 0;
|
||||
|
||||
for (ix = 0; ix < ARRAY_LEN(cdrel_field_registry); ix++) {
|
||||
if (cdrel_field_registry[ix].record_type == type && strcasecmp(cdrel_field_registry[ix].name, name) == 0) {
|
||||
return &cdrel_field_registry[ix];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@ -0,0 +1,230 @@
|
||||
/*
|
||||
* 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 Backend output functions.
|
||||
*
|
||||
* The writers all take a vector of cdrel_value objects and write them to the output
|
||||
* file or database.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "cdrel.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 Write a record to a text file
|
||||
*
|
||||
* Besides being used here, this function is also used by the legacy loggers
|
||||
* that shortcut the advanced stuff.
|
||||
*
|
||||
* \param config The configuration object.
|
||||
* \param record The data to write.
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
int write_record_to_file(struct cdrel_config *config, struct ast_str *record)
|
||||
{
|
||||
FILE *out;
|
||||
int res = 0;
|
||||
|
||||
ast_mutex_lock(&config->lock);
|
||||
if ((out = fopen(config->output_filename, "a"))) {
|
||||
res = fputs(ast_str_buffer(record), out);
|
||||
fputs("\n", out);
|
||||
fclose(out);
|
||||
}
|
||||
ast_mutex_unlock(&config->lock);
|
||||
|
||||
if (!out || res < 0) {
|
||||
ast_log(LOG_ERROR, "Unable to write %s to file %s : %s\n",
|
||||
RECORD_TYPE_STR(config->record_type), config->output_filename, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Concatenate and append a list of values to an ast_str
|
||||
*
|
||||
* \param config Config object
|
||||
* \param values A vector of values
|
||||
* \param str The ast_str to append to
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
static int dsv_appender(struct cdrel_config *config, struct cdrel_values *values, struct ast_str **str)
|
||||
{
|
||||
int ix = 0;
|
||||
int res = 0;
|
||||
|
||||
for (ix = 0; ix < AST_VECTOR_SIZE(values); ix++) {
|
||||
struct cdrel_value *value = AST_VECTOR_GET(values, ix);
|
||||
ast_assert(value->data_type == cdrel_type_string);
|
||||
|
||||
res = ast_str_append(str, -1, "%s%s", ix == 0 ? "" : config->separator, value->values.string);
|
||||
if (res < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Write a DSV list of values to a text file
|
||||
*
|
||||
* \param config Config object
|
||||
* \param values A vector of values
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
static int dsv_writer(struct cdrel_config *config, struct cdrel_values *values)
|
||||
{
|
||||
int res = 0;
|
||||
struct ast_str *str = ast_str_thread_get(&custom_buf, 1024);
|
||||
|
||||
ast_str_reset(str);
|
||||
|
||||
res = dsv_appender(config, values, &str);
|
||||
if (res < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return write_record_to_file(config, str);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Write a list of values as a JSON object to a text file
|
||||
*
|
||||
* \param config Config object
|
||||
* \param values A vector of values
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*
|
||||
* \note We are intentionally NOT using the ast_json APIs here because
|
||||
* they're expensive and these are simple objects.
|
||||
*/
|
||||
static int json_writer(struct cdrel_config *config, struct cdrel_values *values)
|
||||
{
|
||||
int ix = 0;
|
||||
int res = 0;
|
||||
struct ast_str *str = ast_str_thread_get(&custom_buf, 1024);
|
||||
|
||||
ast_str_set(&str, -1, "%s", "{");
|
||||
|
||||
for (ix = 0; ix < AST_VECTOR_SIZE(values); ix++) {
|
||||
struct cdrel_value *value = AST_VECTOR_GET(values, ix);
|
||||
ast_assert(value->data_type == cdrel_type_string);
|
||||
|
||||
res = ast_str_append(&str, -1, "%s\"%s\":%s", ix == 0 ? "" : config->separator, value->field_name, value->values.string);
|
||||
if (res < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
ast_str_append(&str, -1, "%s", "}");
|
||||
|
||||
return write_record_to_file(config, str);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Write a record to a database
|
||||
*
|
||||
* Besides being used here, this function is also used by the legacy loggers
|
||||
* that shortcut the advanced stuff.
|
||||
*
|
||||
* \param config The configuration object.
|
||||
* \param record The data to write.
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
int write_record_to_database(struct cdrel_config *config, struct cdrel_values *values)
|
||||
{
|
||||
int res = 0;
|
||||
int ix;
|
||||
|
||||
ast_mutex_lock(&config->lock);
|
||||
for (ix = 0; ix < AST_VECTOR_SIZE(values); ix++) {
|
||||
struct cdrel_value *value = AST_VECTOR_GET(values, ix);
|
||||
ast_assert(value->data_type == cdrel_type_string);
|
||||
ast_debug(6, "%s '%s'\n", value->field_name, value->values.string);
|
||||
res = sqlite3_bind_text(config->insert, ix + 1, value->values.string, -1, SQLITE_STATIC);
|
||||
if (res != SQLITE_OK) {
|
||||
ast_log(LOG_ERROR, "Unable to write %s to database %s. SQL bind for field %s:'%s'. Error: %s\n",
|
||||
RECORD_TYPE_STR(config->record_type), config->output_filename,
|
||||
value->field_name, value->values.string,
|
||||
sqlite3_errmsg(config->db));
|
||||
sqlite3_reset(config->insert);
|
||||
ast_mutex_unlock(&config->lock);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
res = sqlite3_step(config->insert);
|
||||
if (res != SQLITE_DONE) {
|
||||
ast_log(LOG_ERROR, "Unable to write %s to database %s. Error: %s\n",
|
||||
RECORD_TYPE_STR(config->record_type), config->output_filename,
|
||||
sqlite3_errmsg(config->db));
|
||||
sqlite3_reset(config->insert);
|
||||
ast_mutex_unlock(&config->lock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_reset(config->insert);
|
||||
ast_mutex_unlock(&config->lock);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Write a list of values to a database
|
||||
*
|
||||
* \param config Config object
|
||||
* \param values A vector of values
|
||||
* \retval 0 on success
|
||||
* \retval -1 on failure
|
||||
*/
|
||||
static int database_writer(struct cdrel_config *config, struct cdrel_values *values)
|
||||
{
|
||||
return write_record_to_database(config, values);
|
||||
}
|
||||
|
||||
int load_writers(void)
|
||||
{
|
||||
ast_debug(1, "Loading Writers\n");
|
||||
cdrel_backend_writers[cdrel_format_dsv] = dsv_writer;
|
||||
cdrel_backend_writers[cdrel_format_json] = json_writer;
|
||||
cdrel_backend_writers[cdrel_format_sql] = database_writer;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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 logic for the CDR and CEL Custom Backends
|
||||
*
|
||||
* All source files are in the res/cdrel_custom directory.
|
||||
*
|
||||
* "config.c": Contains common configuration file parsing the ultimate goal
|
||||
* of which is to create a vector of cdrel_config structures for each of
|
||||
* the cdr_custom, cdr_sqlite3_custom, cel_custom and cel_sqlite3_custom
|
||||
* modules. Each cdrel_config object represents an output file defined in
|
||||
* their respective config files. Each one contains a vector of cdrel_field
|
||||
* objects, one for each field in the output record, plus settings like
|
||||
* the output file name, backend type (text file or database), config
|
||||
* type ((legacy or advanced), the field separator and quote character to
|
||||
* use.
|
||||
*
|
||||
* Each cdrel_field object contains an abstract field id that points to
|
||||
* a ast_cdr structure member or CEL event field id along with an input
|
||||
* type and an output type. The registry of cdrel_fields is located in
|
||||
* registry.c.
|
||||
*
|
||||
* "loggers.c": Contains the common "cdrel_logger" entrypoint that the
|
||||
* individual modules call to log a record. It takes the module's
|
||||
* cdrel_configs vector and the record to log it got from the core
|
||||
* cel.c and cdr.c. It then looks up and runs the logger implementation
|
||||
* based on the backend type (text file or database) and config type
|
||||
* (legacy or advanced).
|
||||
*
|
||||
* "getters_cdr.c", "getters_cel.c": Contain the getters that retrieve
|
||||
* values from the ast_cdr or ast_event structures based on the field
|
||||
* id and input type defined for that field and create a cdrel_value
|
||||
* wrapper object for it.
|
||||
*
|
||||
* "writers.c": Contains common backend writers for the text file and
|
||||
* database backends.
|
||||
*
|
||||
* The load-time flow...
|
||||
*
|
||||
* Each of the individual cdr/cel custom modules call the common
|
||||
* cdrel_load_module function with their backend_type, record_type
|
||||
* (cdr or cel), config file name, and the logging callback that
|
||||
* should be registered with the core cdr or cel facility.
|
||||
*
|
||||
* cdrel_load_module calls the config load function appropriate for
|
||||
* the backend type, each of which parses the config file and, if
|
||||
* successful, registers the calling module with the cdr or cel core
|
||||
* and creates a vector of cdrel_config objects that is passed
|
||||
* back to the calling module. That vector contains the context for
|
||||
* all future operations.
|
||||
*
|
||||
* The run-time flow...
|
||||
*
|
||||
* The core cdr and cel modules use their registries of backends and call
|
||||
* the callback function registered by the 4 cdr and cel custom modules.
|
||||
* No changes there.
|
||||
*
|
||||
* Each of those modules call the common cdrel_logger function with their
|
||||
* cdrel_configs vector and the actual ast_cdr or ast_event structure to log.
|
||||
* The cdrel_logger function iterates over the cdrel_configs vector and for
|
||||
* each invokes the logger implementation specific to the backend type
|
||||
* (text file or database) and config type (legacy or advanced).
|
||||
*
|
||||
* For legacy config types, the logger implementation simply calls
|
||||
* ast_str_substitute_variables() on the whole opaque format and writes
|
||||
* the result to the text file or database.
|
||||
*
|
||||
* For advanced configs, the logger implementation iterates over each field
|
||||
* in the cdrel_config's fields vector and for each, calls the appropriate
|
||||
* getter based on the record type (cdr or cel) and field id. Each getter
|
||||
* call returns a cdrel_value object which is then passed to a field formatter
|
||||
* looked up based on the field's data type (string, int32, etc). The formatter
|
||||
* is also passed the cdrel_config object and the desired output type and
|
||||
* returns the final value in another cdrel_value object formatted with any
|
||||
* quoting, etc. needed. The logger accumulates the output cdrel_values
|
||||
* (which are all now strings) in another vector and after all fields have
|
||||
* been processed, hands the vector over to one of the backend writers.
|
||||
*
|
||||
* The backend writer concatenates the cdrel_values into an output record
|
||||
* using the config's separator setting and writes it to the text file
|
||||
* or database. For the JSON output format, it creates a simple
|
||||
* name/value pair output record.
|
||||
*
|
||||
* The identification of field data types, field ids, record types and
|
||||
* backend types is all done at config load time and saved in the
|
||||
* cdrel_config and cdrel_field objects. The callbacks for getters, formatters
|
||||
* and writers are also loaded when the res_cdrel_custom module loads
|
||||
* and stored in arrays indexed by their enum values. The result is that
|
||||
* at run time, simple array indexing is all that's needed to get the
|
||||
* proper getter, formatter and writer for any logging request.
|
||||
*
|
||||
*/
|
||||
|
||||
/*** MODULEINFO
|
||||
<depend>sqlite3</depend>
|
||||
<support_level>core</support_level>
|
||||
***/
|
||||
|
||||
|
||||
#include "asterisk.h"
|
||||
|
||||
#include "asterisk/module.h"
|
||||
#include "cdrel_custom/cdrel.h"
|
||||
|
||||
/*
|
||||
* Populated by cdrel_custom/getters_cdr.c and cdrel_custom/getters_cel.c.
|
||||
*/
|
||||
cdrel_field_getter cdrel_field_getters[cdrel_record_type_end][cdrel_data_type_end];
|
||||
|
||||
/*
|
||||
* Populated by cdrel_custom/formatters.c.
|
||||
*/
|
||||
cdrel_field_formatter cdrel_field_formatters[cdrel_data_type_end];
|
||||
|
||||
/*
|
||||
* Populated by cdrel_custom/writers.c.
|
||||
*/
|
||||
cdrel_backend_writer cdrel_backend_writers[cdrel_format_type_end];
|
||||
|
||||
/*
|
||||
* Populated by cdrel_custom/getters_cdr.c and cdrel_custom/getters_cel.c.
|
||||
*/
|
||||
cdrel_dummy_channel_alloc cdrel_dummy_channel_allocators[cdrel_format_type_end];
|
||||
|
||||
/*
|
||||
* You must ensure that there's an entry for every value in the enum.
|
||||
*/
|
||||
|
||||
const char *cdrel_record_type_map[] = {
|
||||
[cdrel_record_cdr] = "CDR",
|
||||
[cdrel_record_cel] = "CEL",
|
||||
[cdrel_record_type_end] = "!!END!!",
|
||||
};
|
||||
|
||||
const char *cdrel_module_type_map[] = {
|
||||
[cdrel_backend_text] = "Custom ",
|
||||
[cdrel_backend_db] = "SQLITE3 Custom",
|
||||
[cdrel_backend_type_end] = "!!END!!",
|
||||
};
|
||||
|
||||
const char *cdrel_data_type_map[] = {
|
||||
[cdrel_type_string] = "string",
|
||||
[cdrel_type_timeval] = "timeval",
|
||||
[cdrel_type_literal] = "literal",
|
||||
[cdrel_type_amaflags] = "amaflags",
|
||||
[cdrel_type_disposition] = "disposition",
|
||||
[cdrel_type_uservar] = "uservar",
|
||||
[cdrel_type_event_type] = "event_type",
|
||||
[cdrel_type_event_enum] = "event_enum",
|
||||
[cdrel_data_type_strings_end] = "!!STRINGS END!!",
|
||||
[cdrel_type_int32] = "int32",
|
||||
[cdrel_type_uint32] = "uint32",
|
||||
[cdrel_type_int64] = "int64",
|
||||
[cdrel_type_uint64] = "uint64",
|
||||
[cdrel_type_float] = "float",
|
||||
[cdrel_data_type_end] = "!!END!!",
|
||||
};
|
||||
|
||||
enum cdrel_data_type cdrel_data_type_from_str(const char *str)
|
||||
{
|
||||
enum cdrel_data_type data_type = 0;
|
||||
for (data_type = 0; data_type < cdrel_data_type_end; data_type++) {
|
||||
if (strcasecmp(cdrel_data_type_map[data_type], str) == 0) {
|
||||
return data_type;
|
||||
}
|
||||
|
||||
}
|
||||
return cdrel_data_type_end;
|
||||
}
|
||||
|
||||
static const char *cdrel_field_flags_map[] = {
|
||||
[CDREL_FIELD_FLAG_QUOTE] = "quote",
|
||||
[CDREL_FIELD_FLAG_NOQUOTE] = "noquote",
|
||||
[CDREL_FIELD_FLAG_TYPE_FORCED] = "type_forced",
|
||||
[CDREL_FIELD_FLAG_USERVAR] = "uservar",
|
||||
[CDREL_FIELD_FLAG_LITERAL] = "literal",
|
||||
[CDREL_FIELD_FLAG_FORMAT_SPEC] = "format_spec",
|
||||
[CDREL_FIELD_FLAG_LAST] = "LAST",
|
||||
};
|
||||
|
||||
const char *cdrel_get_field_flags(struct ast_flags *flags, struct ast_str **str)
|
||||
{
|
||||
int ix = 0;
|
||||
int res = 0;
|
||||
int trues = 0;
|
||||
|
||||
for (ix = 0; ix < CDREL_FIELD_FLAG_LAST; ix++) {
|
||||
if (ast_test_flag(flags, (1 << ix))) {
|
||||
res = ast_str_append(str, -1, "%s%s", trues++ ? "," : "", cdrel_field_flags_map[ix]);
|
||||
if (res < 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast_str_buffer(*str);
|
||||
}
|
||||
|
||||
|
||||
const char *cdrel_basename(const char *path)
|
||||
{
|
||||
int i = 0;
|
||||
const char *basename = path;
|
||||
|
||||
if (ast_strlen_zero(path)) {
|
||||
return path;
|
||||
}
|
||||
i = strlen(path) - 1;
|
||||
while(i >= 0) {
|
||||
if (path[i] == '/') {
|
||||
basename = &path[i + 1];
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
return basename;
|
||||
}
|
||||
|
||||
static int unload_module(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum ast_module_load_result load_module(void)
|
||||
{
|
||||
load_cdr();
|
||||
load_cel();
|
||||
load_formatters();
|
||||
load_writers();
|
||||
return AST_MODULE_LOAD_SUCCESS;
|
||||
}
|
||||
|
||||
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER,
|
||||
"Combined logic for CDR/CEL Custom modules",
|
||||
.support_level = AST_MODULE_SUPPORT_CORE,
|
||||
.load = load_module,
|
||||
.unload = unload_module,
|
||||
.load_pri = AST_MODPRI_CDR_DRIVER,
|
||||
);
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
global:
|
||||
LINKER_SYMBOL_PREFIX*cdrel_*;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
Loading…
Reference in new issue