Mod xml cdr

From FreeSWITCH Wiki
Jump to: navigation, search

Contents

Description

Outputs CDR records in XML format to local file or to a webserver via HTTP POST.

Here is an Example XML CDR

Informational Tip

make sure you've updated to trunk revision 5456 or later.


 


Features

Output capabilities

  • Always log to the disk
  • Always log to HTTP/HTTPS
  • Always log to both

Reliability capabilities

On failure it will log the HTTP POST data to disk where you specify, and it has configurable timeouts and retries for the HTTP post.

Getting it working

modules.conf

To compile the module, uncomment mod_xml_cdr in modules.conf

Install dependencies

If you try to build and get an error

Error

mod_xml_cdr.c:34:23: error: curl/curl.h: No such file or directory

you will need to install curl and the development headers.

Debian

Type This
apt-get install libcurl3-dev libcurl3


Building

Type This
make install


modules.conf.xml

Load the module in conf/autoload_configs/modules.conf.xml.

<load module="mod_xml_cdr"/>

Type "load mod_xml_cdr" at the CLI to load the module.

Sample Configuration

Edit or Create a file in the conf/autoload_configs directory called xml_cdr.conf.xml with the following contents:

Edit This
<configuration name="xml_cdr.conf" description="XML CDR CURL logger">
<settings>
   <param name="cred" value="user:pass"/>
   <param name="url" value="http://myhost/cdr.php"/>
   <param name="retries" value="2"/>
   <param name="delay" value="120"/>
   <param name="log-dir" value="/var/log/cdr"/>
   <param name="err-log-dir" value="/var/log/cdr/errors"/>
   <param name="encode" value="True"/>
</settings>

 


Informational Tip

  • the directories you specify for log-dir or err-log-dir must exist.
  • you will have to create your own HTTP CGI handler in place of cdr.php. See contrib/trixter/xml-cdr to view the code of the example cdr.php.
  • double check freeswitch.xml and make sure xml_cdr.conf.xml is included. If not, it means you need to get a newer version of freeswitch for this feature to work.

 


Default Configuration that comes with GIT as of 2011-03-11:

<configuration name="xml_cdr.conf" description="XML CDR CURL logger">
  <settings>
    <!-- the url to post to if blank web posting is disabled  -->
    <!-- <param name="url" value="http://localhost/cdr_curl/post.php"/> -->

    <!-- optional: credentials to send to web server -->
    <!--    <param name="cred" value="user:pass"/> -->

    <!-- the total number of retries (not counting the first 'try') to post to webserver incase of failure -->
    <!-- <param name="retries" value="2"/> -->

    <!-- delay between retries in seconds, default is 5 seconds -->
    <!-- <param name="delay" value="1"/> -->

    <!-- Log via http and on disk, default is false -->
    <!-- <param name="log-http-and-disk" value="true"/> -->

    <!-- optional: if not present we do not log every record to disk -->
    <!-- either an absolute path, a relative path assuming ${prefix}/logs or a blank value will default to ${prefix}/logs/xml_cdr -->
    <param name="log-dir" value=""/>

    <!-- optional: if not present we do log the b leg -->
    <!-- true or false if we should create a cdr for the b leg of a call-->
    <param name="log-b-leg" value="false"/>
    
    <!-- optional: if not present, all filenames are the uuid of the call -->
    <!-- true or false if a leg files are prefixed "a_" -->
    <param name="prefix-a-leg" value="true"/>

    <!-- encode the post data may be 'true' for url encoding, 'false' for no encoding or 'base64' for base64 encoding -->
    <param name="encode" value="true"/>

    <!-- optional: set to true to disable Expect: 100-continue lighttpd requires this setting --> 
    <!--<param name="disable-100-continue" value="true"/>--> 
    
    <!-- optional: full path to the error log dir for failed web posts if not specified its the same as log-dir -->
    <!-- either an absolute path, a relative path assuming ${prefix}/logs or a blank or omitted value will default to ${prefix}/logs/xml_cdr -->
    <!-- <param name="err-log-dir" value="/tmp"/> -->

    <!-- which auhtentification scheme to use. Supported values are: basic, digest, NTLM, GSS-NEGOTIATE or "any" for automatic detection -->
    <!--<param name="auth-scheme" value="basic"/>--> 

    <!-- optional: this will enable the CA root certificate check by libcurl to
         verify that the certificate was issued by a major Certificate Authority.
         note: default value is disabled. only enable if you want this! -->
    <!--<param name="enable-cacert-check" value="true"/>-->
    <!-- optional: verify that the server is actually the one listed in the cert -->
    <!-- <param name="enable-ssl-verifyhost" value="true"/> -->

    <!-- optional: these options can be used to specify custom SSL certificates
         to use for HTTPS communications. Either use both options or neither.
         Specify your public key with 'ssl-cert-path' and the private key with
         'ssl-key-path'. If your private key has a password, specify it with
         'ssl-key-password'. -->
    <!-- <param name="ssl-cert-path" value="$${base_dir}/conf/certs/public_key.pem"/> -->
    <!-- <param name="ssl-key-path" value="$${base_dir}/conf/certs/private_key.pem"/> -->
    <!-- <param name="ssl-key-password" value="MyPrivateKeyPassword"/> -->

    <!-- optional: use a custom CA certificate in PEM format to verify the peer
         with. This is useful if you are acting as your own certificate authority.
         note: only makes sense if used in combination with "enable-cacert-check." -->
    <!-- <param name="ssl-cacert-file" value="$${base_dir}/conf/certs/cacert.pem"/> -->

    <!-- optional: specify the SSL version to force HTTPS to use. Valid options are
         "SSLv3" and "TLSv1". Otherwise libcurl will auto-negotiate the version. -->
    <!-- <param name="ssl-version" value="TLSv1"/> -->

    <!-- optional: enables cookies and stores them in the specified file. -->
    <!-- <param name="cookie-file" value="/tmp/cookie-mod_xml_curl.txt"/> -->
  </settings>
</configuration>

Verifying

Place a call, hangup, and you should have a new XML file in /usr/local/freeswitch/log. Also check your webserver logs.

Configuration Options

Name Description Example
log-b-leg Whether to log B legs (by default only A legs are logged) true
url Webserver URL (you can specify this element more than once, and it will try each URL in order of succession until retries is reached. note retries is not per URL specified). Note URL is optional, mod_xml_cdr can write only to disk. http://myhost/cdr.php
cred credentials to use to post to webserver (if webserver requires it) user:pass
auth-scheme which authentication scheme to use. Supported values are: basic, digest, NTLM, GSS-NEGOTIATE or "any" for automatic detection basic
encode whether to encode the information in the POST that is sent to the webserver. By default, (if this param is not provided or is invalid) the content-type of the post is application/x-www-form-plaintext and no URL-encoding is done on the POST'ed data. If set to 'True', it will set the content type to application/x-www-form-urlencoded and URL-encode the information. This is what most webservers expect. If set to base64, the content type will be application/x-www-form-base64-encoded. Finally, if set to textxml the content type will be text/xml. base64
retries number of retries before giving up http post and writing to disk 2
timeout HTTP request timeout 20
delay delay in seconds before retrying 120
enable-cacert-check Whether to check server's SSL certificate against CA certificates (recommended for HTTPS) true
enable-ssl-verifyhost Whether to check server's SSL certificate matches the hostname of the URL (recommended for HTTPS) true
ssl-cacert-file Path to CA certificate(s) file. Note this appears to be required as the module doesn't automatically search for system CAs. /etc/ssl/certs/ca-certificates.crt
ssl-cert-path Path to client certificate /etc/ssl/certs/fs_client.crt
ssl-key-path Path to client private key /etc/ssl/private/fs_client.key
ssl-key-password Password for client private key mysecret
ssl-version Which version of SSL/TLS to use. Support values are "SSLv3" or "TLSv1" TLSv1
disable-100-continue Disable 100 Continue in HTTP Expect header, for servers which dislike this option. true
log-http-and-disk Default behaviour is to write either HTTP or Disk on HTTP failure. Setting this to true will write to both HTTP and Disk regardless (handy for realtime + reconciliation later if required) true
log-dir directory to log to -- you must create this directory. If left blank, will use the default directory. If omitted, disables logging to a file and will only POST to webserver. If a name is given instead of a path, a subdirectory under the default will be used. This directory must already exists. /tmp/log
err-log-dir directory to log errors to -- you must create this directory. If left blank, will use the default directory. If omitted, disables logging to a file and will only POST to webserver. If a name is given instead of a path, a subdirectory under the default will be used. This directory must already exists. /tmp/errlog
prefix-a-leg Prefix A leg filesnames with a_, which differentiates A and B leg CDRs on disk. true
rotate Whether to rotate the directory CDRs are written to. If enabled logs to a yyyy-mm-dd-hh-mm-ss subdirectory of log-dir created when the module loads, and rotates to new directory on SIGHUP. Having a large number of files in a directory can become a performance bottleneck, this limits when trying to open/create paths in/under that directory, so this can improve performance when logging to disk. true

Handling POST request

The POST request can be made to any web server via HTTP or HTTPS. The script handling the request can be in any language you choose.

The POST request consists of a single variable named cdr, which contains the CDR as a XML document (the same data that would be written to disk).

If "prefix-a-leg" is set to true, then the URL that is posted to will be contain in the GET string: "?uuid=a_$uuid" for the a leg.

If the page returns a HTTP status code of 200 OK, then the request is treated as successful. Any other status code will be treated as failure, which will cause mod_xml_cdr to either retry the request, try the next server or write to disk. No output is required; only the status code is used to check whether the request succeeded.

The contents of the cdr variable can be parsed with the XML parser available within your language of choice. It will appear similar to:

 <?xml version="1.0"?>
 <cdr>
   <channel_data>
     <state>CS_REPORTING</state>
     <direction>inbound</direction>
     <state_number>11</state_number>
     <flags>0=1;36=1;38=1;51=1</flags>
     <caps>1=1;2=1;3=1</caps>
   </channel_data>
   <variables>
    <uuid>2e831835-d336-4735-b3e5-90e5d7dc8187</uuid>
    <sip_network_ip>192.168.0.2</sip_network_ip>
    <sip_network_port>56866</sip_network_port>
    <sip_received_ip>192.168.0.2</sip_received_ip>
    <sip_received_port>56866</sip_received_port>
    <sip_via_protocol>udp</sip_via_protocol>
    <sip_from_user>1000</sip_from_user>
    <sip_from_uri>1000%40192.168.0.2</sip_from_uri>
    <sip_from_host>192.168.0.2</sip_from_host>
    <sip_from_user_stripped>1000</sip_from_user_stripped>
    <sip_from_tag>BD37552C-4B5</sip_from_tag>
    ...
   </variables>
   <app_log>
    <application app_name="set" app_data="continue_on_fail=true"></application>
    <application app_name="bridge" app_data="sofia/external/gateway/gw001/1000"></application>
    <application app_name="bridge" app_data="sofia/external/gateway/gw002/1000"></application>
   </app_log>
   <callflow dialplan="XML" profile_index="1">
    <extension name="1000" number="1000">
     <application app_name="set" app_data="continue_on_fail=true"></application>
     <application app_name="bridge" app_data="sofia/external/gateway/gw001/1000"></application>
     <application app_name="bridge" app_data="sofia/external/gateway/gw002/1000"></application>
     <application app_name="bridge" app_data="sofia/external/gateway/gw003/1000"></application>
     <application app_name="bridge" app_data="sofia/external/gateway/gw004/1000"></application>
     <application app_name="bridge" app_data="sofia/external/gateway/gw005/1000"></application>
    </extension>
   </callflow>
   <caller_profile>
     <username>1000</username>
     <dialplan>XML</dialplan>
     <caller_id_name>1000</caller_id_name>
     <ani>1000</ani>
     <aniii></aniii>
     <caller_id_number>1000</caller_id_number>
     <network_addr>192.168.0.2</network_addr>
     <rdnis>1000</rdnis>
     <destination_number>1000</destination_number>
     <uuid>2e831835-d336-4735-b3e5-90e5d7dc8187</uuid>
     <source>mod_sofia</source>
     <context>default</context>
     <chan_name>sofia/default/1000@192.168.0.2</chan_name>
   </caller_profile>
   <times>
     <created_time>1274439432438053</created_time>
     <profile_created_time>1274439432448060</profile_created_time>
     <progress_time>0</progress_time>
     <progress_media_time>0</progress_media_time>
     <answered_time>0</answered_time>
     <hangup_time>1274439438418776</hangup_time>
     <resurrect_time>0</resurrect_time>
     <transfer_time>0</transfer_time>
   </times>
 </cdr>

Apparently, all values are URL encoded. This is not part of XML like the FreeSWITCH wiki previously asserted. It's just that by URL encoding the value, you remove the usage of characters that'd break the XML value (&,<,>), and no further escaping is needed. Thus, when reading the value of any element, you need to URI decode. A proper XML parser will not do this for you: you will get an encoded value back. In the above example, an XML parser would return "1000%40192.168.0.2" for the variable sip_from_uri. If someone knows the reason for using URL encoding here instead of just using XML (which would save quite a few bytes, especially with a failed bleg XML CDR encoded into a variable), please add to the wiki.

Examples

See scripts/contrib/trixter/xml-cdr for examples to take a webpost (which mod_xml_cdr does now) and turn it into an associative array so your script can process it.


Process xml_cdr Post with .Net MVC3

        [HttpPost]
        public void CDR(string uuid)
        {
            var auth = ASCIIEncoding.ASCII.GetString(
                       Convert.FromBase64String(Request.Headers["AUTHORIZATION"]));

            // TODO: Using auth check against your DB or whatever if user:pass is authorized.

            // Get the xml from the input stream.
            StreamReader r = new StreamReader(Request.InputStream);
            string xml = r.ReadToEnd();
            // Strips some unwanted chars.
            xml.Substring(4, xml.Length - 4);

            // Parse the string. Used XElement below but you could use XmlDocument also.
            XElement elm = XElement.Parse(xml);

            // Now using xpath or LINQ grab the element values you need.
            // For ex: YOUR_ELEMENT could = variables/hangup_cause.
            string selectedElm = Uri.UnescapeDataString(elm.XPathSelectElement("//" + "YOUR_ELEMENT").Vmialue); 
        }

Resubmitting failed CDR submissions

Within ./conf/autoload_configs/xml_cdr.conf.xml you can activate the "err-log-dir" option to log in a directory the xml cdrs if the http cdr server is down.

When the error that prevented the CDR being submitted is corrected you will want to resend the CDR to the server.

The curl command to do that is :

 curl --request POST --fail --data-urlencode cdr@$CDR http://$CDR_SERVER_URI/ >/dev/null \

Script (for cronjob or manual run)

This script is suitable for running from the commandline or from a cronjob. It reads all the required server details from the mod_xml_cdr configuration file.

You will need libxml2-utils and curl installed.

#!/bin/sh

# Configuration file location
# You may want to tweak this, depending on your local installation environment.
CONFIG_FILE=/etc/freeswitch/autoload_configs/xml_cdr.conf.xml

# Read config

get_param()
{
    NAME=$1
    xmllint $CONFIG_FILE -xpath "string(/configuration/settings/param[@name='$NAME']/@value)"
}

CDRDIR=$(get_param err-log-dir)
URL=$(get_param url)
AUTH_SCHEME=$(get_param auth-scheme)
CRED=$(get_param cred)

# Resubmit any CDRs in error directory. Remove files on successful HTTP request.
# Note error CDRs are stored already urlencoded
for CDR in $(find $CDRDIR -name "*.cdr.xml"); do
    echo "Resubmitting $(basename $CDR .cdr.xml)..."
    echo -n 'cdr='$(cat $CDR) | \
        curl -sS -X POST -f --$AUTH_SCHEME -u $CRED -d @- $URL >/dev/null \
            && echo "OK, removing CDR file" \
            && rm -f $CDR
done

# The following is suitable for uploading CDRs that were logged to disk
# (these are not urlencoded yet so must be handled differently)
#for CDR in $(find $CDRDIR -name "*.cdr.xml"); do
#        echo "Resubmitting $(basename $CDR .cdr.xml)..."
#        curl -sS --request POST --fail --$AUTH_SCHEME --user $CRED --data-urlencode cdr@$CDR $URL >/dev/null \
#                && echo "OK, removing CDR file" \
#                && rm -f $CDR
#done

Note this script assumes you are using basic/digest password authentication. If you are using no authentication or client certificates then this script will require modifications.

Reference Information

The Example XML CDR was generated by calling the FreeSWITCH conference via the PSTN. A SIP phone registered with FS dialed 9+2137991400 and went through this dialplan extension:

  <!-- Dial 9, go out Qwest PRI on span 1 -->
  <extension name="Dial 9">
    <condition field="destination_number" expression="^9(\d{10})$">
      <action application="bridge" data="freetdm/1/A/$1"/>
    </condition>
  </extension>

Channel Variables

The XML CDR contains a number of channel variables as well as call flow information.
Call times are in date/time stamp format or in either epoch seconds or epoch microseconds (usec).
Date/time stamp variables are URI encoded so be sure to "URI decode" them prior to inserting them into a database.
Example: "2008-07-31%2011%3A35%3A38" is URI encoded. Decoded it is "2008-07-31 11:35:38".
The epoch began at 1970-01-01 00:00:00, therefore the epoch values are the number of seconds (or microseconds) since the epoch began.
Using the epoch microseconds values allows for extremely accurate measurements.

Call Date/Time Variables

answer_stamp

Date/time that the far end of the call was actually answered.
In the example CDR this value is "2008-07-31%2011%3A35%3A41" (2008-07-31 11:35:41).

end_stamp

Date/time that the call was terminated.
In the example CDR this value is "2008-07-31%2011%3A36%3A17" (2008-07-31 11:36:17).

profile_start_stamp
progress_stamp

Date/time that progress information from the far end is first received.

progress_media_stamp

Date/time that "early media" or progress media was first received. "Early media" includes ring-back tone (RBT).
In the example CDR this value is "2008-07-31%2011%3A35%3A38" (2008-07-31 11:35:38).

start_stamp

Date/time that the call was initiated.
In the example CDR this value is "2008-07-31%2011%3A35%3A38" (2008-07-31 11:35:38).

Call Epoch Variables

answer_epoch

Call answer time in epoch seconds.
In the example CDR this value is 1217529338.

answer_uepoch

Call answer time in epoch microseconds.
In the example CDR this value is 1217529338452698.

end_epoch

Call end time in epoch seconds.
In the example CDR this value is 1217529377.

end_uepoch

Call end time in epoch microseconds.
In the example CDR this value is 1217529377795951.

profile_start_epoch

Profile start time in epoch seconds.
In the example CDR this value is 1217529338.

profile_start_uepoch

Profile start time in epoch microseconds.
In the example CDR this value is 1217529338212616.

start_epoch

Call start time in epoch seconds.
In the example CDR this value is 1217529338.

start_uepoch

Call start time in epoch microseconds.
In the example CDR this value is 1217529338212616.

Call Duration Variables

billsec

Billable call duration in seconds. Billable time does not include call time spent in "early media" prior to the far end answering the call.
In the example CDR this value is 36.

billmsec

Billable call duration in milliseconds. Billable time does not include call time spent in "early media" prior to the far end answering the call.
In the example CDR this value is 36267.

billusec

Billable call duration in microseconds. Billable time does not include call time spent in "early media" prior to the far end answering the call.
In the example CDR this value is 36267529.

duration

Total call duration in seconds.
In the example CDR this value is 39.

flow_billsec
flow_billmsec
flow_billusec
mduration

Total call duration in milliseconds.
In the example CDR this value is 39583.

progresssec

The amount of time in seconds between the sip invite and the sip 180 message
In the example CDR this value is 0. (Less than 1 second.)

progressmsec

The amount of time in milliseconds between the sip invite and the sip 180 message
In the example CDR this value is 238.

progressusec

The amount of time in microseconds between the sip invite and the sip 180 message
In the example CDR this value is 237955.

progress_mediasec

The amount of time in seconds between the sip invite and the sip 183 message
In the example CDR this value is 0. (Less than 1 second.)

progress_mediamsec

The amount of time in milliseconds between the sip invite and the sip 183 message
In the example CDR this value is 238.

progress_mediausec

The amount of time in microseconds between the sip invite and the sip 183 message
In the example CDR this value is 237955.

uduration

Total call duration in microseconds.
In the example CDR this value is 39583335.

Troubleshooting

Error loading module

2007-06-27 17:59:12 [ERR] switch_loadable_module.c:714 switch_loadable_module_load_file() Error 
Loading module /usr/local/freeswitch/mod/mod_xml_cdr.so
**/usr/local/freeswitch/mod/mod_xml_cdr.so: undefined symbol: curl_easy_getinfo**

Fix: This indicates you dont have curl linked in right. Check the mod_xml_cdr/Makefile and make sure it has WANTS_CURL=yes

Does not write to non-default logDir

If you set logDir to /tmp/foo, you must mkdir /tmp/foo/xml_cdr or it will revert to the default directory.

I'm getting a Memory Error when module loads

2007-06-27 19:41:06 [DEBUG] mod_xml_cdr.c:192 do_config() processing logDir config
2007-06-27 19:41:06 [DEBUG] mod_xml_cdr.c:197 do_config() val is NON zero length, setting globals.logDir: /tmp/log
2007-06-27 19:41:06 [CRIT] mod_xml_cdr.c:219 do_config() Memory Error!

Fix: try setting a custom errLogDir in config and this error should go away. It is a bug, and it's not actually a memory error that indicates any memory related problems.

Example CGI Script

The following is a simple CGI script written in Perl that demonstrates what you can do with the POSTed XML CDR data that comes from mod_xml_cdr. Here are the requirements:

  • Web server (tested w/ Apache)
  • Script in a directory w/ execute permissions (tested with /cgi-bin on my Apache install)
  • Write access to a directory (tested with /tmp/ on my system)
  • XML::Simple from CPAN (run cpan from CLI, then "install XML::Simple")
  • CGI::Simple from CPAN (run cpan from CLI, then "install CGI::Simple")

I used this config option in my xml_cdr.conf.xml:

<param name="url" value="http://localhost/cgi-bin/fs-xml-cdr.pl"/>

When the XML CDR gets POSTed to the Web server the script gets launched. I think you'll find the comments are rather generous and give you lots of help. The ultimate result is that you have /tmp/fs-xml-cdr.log getting written with all sorts of interesting pieces of information, plus you have an example of turning the raw XML into a data structure as well as examples of creating a CSV record and a SQL INSERT statement.

Code Listing [Perl]

#
# fs-xml-cdr.pl
#
# Example of a daemon that will sit and collect XML CDR info and spit out certain stuff for you to tinker with
#  Can also be used to bootstrap into a direct-to-db process

use strict;
use warnings;

use XML::Simple; # Get from CPAN
use CGI::Simple; # Get from CPAN
use Data::Dumper;

# dump into a place for further review
open(FILEOUT,">>",'/tmp/fs-xml-cdr.log');

# $cgi object has handy methods
my $cgi = new CGI::Simple;

# get the actual cdr stuff from the CGI call
my $cdr = $cgi->param('cdr');

# Dump raw XML to file
print FILEOUT "Raw XML CDR:\n\n";
print FILEOUT $cdr . "\n";

# put cdr info into a simple object
my $xml_cdr;
eval { $xml_cdr = XMLin($cdr, ForceArray => 1); };

if ( $@ ) {
  print FILEOUT "Error parsing XML: $@\n";
}
# Access cdr info
print FILEOUT "My leg UUID is: " . $xml_cdr->{variables}->[0]->{uuid}->[0] . "\n";
print FILEOUT "Other leg UUID is: " . $xml_cdr->{variables}->[0]->{bridge_uuid}->[0] . "\n\n";

# Check out the whole data structure
print FILEOUT "\$xml_cdr looks like this:\n\n";
print FILEOUT Dumper($xml_cdr) . "\n";

# URL decode
$cdr = $cgi->url_decode($cdr);

# Dump URL decoded XML
print FILEOUT "Decoded XML CDR:\n\n";
print FILEOUT $cdr . "\n";

# Dump specific fields into a CSV file or into a database or whatever you want
# Add the fields to this array in the order in which you want them to appear in your db or CSV
# Add/remove field names as you see fit
my @fields = (
                          'uuid',
                          'bridge_uuid',
                          'direction',
                          'billsec',
                          'start_epoch',
                          'end_epoch',

);

my @data;  # field data here
my $csv;   # formatted csv record
my $sql;   # sql insert statement (assumes table field names match those in @fields)

# Build a @data array
foreach (0..$#fields) {
  my $field = $fields[$_];
  # If you need to do some special formatting for any field then do it here
  # Otherwise just drop it into the array
  push @data,$xml_cdr->{variables}->[0]->{$field}->[0];
}

# Build a simple CSV record
$csv = join ",",@data;
print FILEOUT "CSV record looks like this: \n";
print FILEOUT $csv . "\n\n";

# Build a simple SQL insert statement
$sql  = 'INSERT INTO cdr(';
$sql .= join ",",@fields;
$sql .= ") VALUES ('";
$sql .= join "','",@data;
$sql .= "')";
print FILEOUT "SQL INSERT command looks like this: \n";
print FILEOUT $sql . "\n\n";

print FILEOUT "= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =\n\n";
close(FILEOUT);

FAQ

Q: Does mod_cdr need to be compiled/enabled?

No, this module is independent -- you may have both without conflict, they will not communicate with each other.