mod_xml_cdr
About
Outputs CDR records in XML format to local file or to a webserver via HTTP POST.
Click here to expand Table of Contents
- 1 Example
- 2 Features
- 3 Getting it working
- 3.1 modules.conf
- 3.2 Install dependencies
- 3.3 Sample Configuration
- 3.3.1 Verifying
- 3.4 Configuration Options
- 3.5 Handling POST request
- 3.5.1 Examples
- 3.6 Resubmitting failed CDR submissions
- 3.7 Reference Information
- 3.7.1 Channel Variables
- 3.8 Troubleshooting
- 3.9 Example CGI Script
- 3.9.1 Code Listing [Perl]
- 3.10 FAQ
Example
Here is an Example XML CDR:
Example XML CDR
<?xml version="1.0"?> <cdr> <variables> <sip_received_ip>192.168.50.21</sip_received_ip> <sip_received_port>39368</sip_received_port> <sip_authorized>true</sip_authorized> <sip_mailbox>1160</sip_mailbox> <sip_auth_username>1160</sip_auth_username>
<sip_auth_realm>192.168.50.229</sip_auth_realm>
<mailbox>1160</mailbox>
<record_stereo>true</record_stereo>
<accountcode>1160</accountcode>
<user_context>default</user_context>
<effective_caller_id_name>Michael%20S%20Collins</effective_caller_id_name>
<effective_caller_id_number>5597993757</effective_caller_id_number>
<sip_from_user>1160</sip_from_user>
<sip_from_uri>1160%40192.168.50.229</sip_from_uri>
<sip_from_host>192.168.50.229</sip_from_host>
<sip_from_user_stripped>1160</sip_from_user_stripped>
<sip_from_tag>a360bd54</sip_from_tag>
<sofia_profile_name>internal</sofia_profile_name>
<sofia_profile_domain_name>192.168.50.229</sofia_profile_domain_name>
<sip_req_user>92137991400</sip_req_user>
<sip_req_uri>92137991400%40192.168.50.229</sip_req_uri>
<sip_req_host>192.168.50.229</sip_req_host>
<sip_to_user>92137991400</sip_to_user>
<sip_to_uri>92137991400%40192.168.50.229</sip_to_uri>
<sip_to_host>192.168.50.229</sip_to_host>
<sip_contact_user>1160</sip_contact_user>
<sip_contact_port>39368</sip_contact_port>
<sip_contact_uri>1160%40192.168.50.21%3A39368</sip_contact_uri>
<sip_contact_host>192.168.50.21</sip_contact_host>
<channel_name>sofia/internal/1160%40192.168.50.229</channel_name>
<sip_call_id>NzUyYzU2ZmU0NjQ0NmNkMGExYTNiZjIzMjIwMWNiODc.</sip_call_id>
<sip_via_host>192.168.50.21</sip_via_host>
<sip_via_port>39368</sip_via_port>
<sip_via_rport>39368</sip_via_rport>
<max_forwards>70</max_forwards>
<presence_id>1160%40192.168.50.229</presence_id>
<switch_r_sdp>v%3D0%0D%0Ao%3D-%206%202%20IN%20IP4%20192.168.50.21%0D%0As%3DCounterPath%20X-Lite%203.0%0D%0Ac%3DIN%20IP4%20192.168.50.21%0D%0At%3D0%200%0D%0Am%3Daudio%209140%20RTP/AVP%20107%20119%20100%20106%200%20105%2098%208%203%20101%0D%0Aa%3Drtpmap%3A107%20BV32/16000%0D%0Aa%3Drtpmap%3A119%20BV32-FEC/16000%0D%0Aa%3Drtpmap%3A100%20SPEEX/16000%0D%0Aa%3Drtpmap%3A106%20SPEEX-FEC/16000%0D%0Aa%3Drtpmap%3A105%20SPEEX-FEC/8000%0D%0Aa%3Drtpmap%3A98%20iLBC/8000%0D%0Aa%3Drtpmap%3A101%20telephone-event/8000%0D%0Aa%3Dfmtp%3A101%200-15%0D%0Aa%3Dalt%3A1%201%20%3A%20CV3qZAA1%209gFf2/Iz%20192.168.50.21%209140%0D%0A</switch_r_sdp>
<remote_media_ip>192.168.50.21</remote_media_ip>
<remote_media_port>9140</remote_media_port>
<write_codec>PCMU</write_codec>
<write_rate>8000</write_rate>
<open>true</open>
<use_profile>nat</use_profile>
<numbering_plan>US</numbering_plan>
<default_gateway>asterlink.com</default_gateway>
<default_area_code>559</default_area_code>
<user_name>default</user_name>
<domain_name>192.168.50.229</domain_name>
<local_media_ip>192.168.50.229</local_media_ip>
<local_media_port>9140</local_media_port>
<current_application>bridge</current_application>
<originate_disposition>SUCCESS</originate_disposition>
<bridge_channel>OpenZAP/1%3A23/2137991400</bridge_channel>
<bridge_uuid>e012ce96-7f9c-4194-87fe-5ba86a3930ca</bridge_uuid>
<signal_bond>e012ce96-7f9c-4194-87fe-5ba86a3930ca</signal_bond>
<sip_nat_detected>true</sip_nat_detected>
<endpoint_disposition>ANSWER</endpoint_disposition>
<sip_user_agent>X-Lite%20release%201100l%20stamp%2047546</sip_user_agent>
<sip_term_status>200</sip_term_status>
<sip_term_cause>16</sip_term_cause>
<hangup_cause>NORMAL_CLEARING</hangup_cause>
<start_stamp>2008-07-31%2011%3A35%3A38</start_stamp>
<profile_start_stamp>2008-07-31%2011%3A35%3A38</profile_start_stamp>
<answer_stamp>2008-07-31%2011%3A35%3A41</answer_stamp>
<progress_stamp>2008-07-31%2011%3A35%3A38</progress_stamp>
<progress_media_stamp>2008-07-31%2011%3A35%3A38</progress_media_stamp>
<end_stamp>2008-07-31%2011%3A36%3A17</end_stamp>
<start_epoch>1217529338</start_epoch>
<start_uepoch>1217529338212616</start_uepoch>
<profile_start_epoch>1217529338</profile_start_epoch>
<profile_start_uepoch>1217529338212616</profile_start_uepoch>
<answer_epoch>1217529338</answer_epoch>
<answer_uepoch>1217529338452698</answer_uepoch>
<end_epoch>1217529377</end_epoch>
<end_uepoch>1217529377795951</end_uepoch>
<last_app>bridge</last_app>
<last_arg>openzap/1/A/2137991400</last_arg>
<caller_id>%22Mikey%22%20%3C1160%3E</caller_id>
<duration>39</duration>
<billsec>36</billsec>
<progresssec>0</progresssec>
<progress_mediasec>0</progress_mediasec>
<flow_billsec>39</flow_billsec>
<mduration>39583</mduration>
<billmsec>36267</billmsec>
<progressmsec>238</progressmsec>
<progress_mediamsec>237955</progress_mediamsec>
<flow_billmsec>39583</flow_billmsec>
<uduration>39583335</uduration>
<billusec>36267529</billusec>
<progressusec>237955</progressusec>
<progress_mediausec>237955</progress_mediausec>
<flow_billusec>39583335</flow_billusec>
<read_codec>PCMU</read_codec>
<read_rate>8000</read_rate>
</variables> <app_log> <application app_name="set" app_data="open=true"></application> <application app_name="set" app_data="use_profile=nat"></application> <application app_name="set_user" app_data="default@192.168.50.229"></application> <application app_name="db" app_data="insert/spymap/1160/6f32e2f8-38d1-43a7-b6ea-215bbfe6a314"></application> <application app_name="db" app_data="insert/last_dial/1160/92137991400"></application> <application app_name="db" app_data="insert/last_dial/global/6f32e2f8-38d1-43a7-b6ea-215bbfe6a314"></application> <application app_name="record_session" app_data="/mnt/powervault/Databases/XMLCDR/6f32e2f8-38d1-43a7-b6ea-215bbfe6a314__1.wav"></application>
<application app_name="bridge" app_data="openzap/1/A/2137991400"></application>
</app_log> <callflow dialplan="XML" profile_index="1"> <extension name="tod_example" number="92137991400"> <application app_name="set" app_data="open=true"></application> <application app_name="set" app_data="use_profile=${cond(${acl(${network_addr} rfc1918)} == true ? nat : default)}"></application> <application app_name="set_user" app_data="default@${domain}"></application> <application app_name="db" app_data="insert/spymap/${caller_id_number}/${uuid}"></application> <application app_name="db" app_data="insert/last_dial/${caller_id_number}/${destination_number}"></application>
<application app_name="db" app_data="insert/last_dial/global/${uuid}"></application>
<application app_name="record_session" app_data="/mnt/powervault/Databases/XMLCDR/${uuid}__1.wav"></application>
<application app_name="bridge" app_data="openzap/1/A/2137991400"></application>
</extension>
<caller_profile>
<username>1160</username>
<dialplan>XML</dialplan>
<caller_id_name>Mikey</caller_id_name>
<ani></ani>
<aniii></aniii>
<caller_id_number>1160</caller_id_number>
<network_addr>192.168.50.21</network_addr>
<rdnis></rdnis>
<destination_number>92137991400</destination_number>
<uuid>6f32e2f8-38d1-43a7-b6ea-215bbfe6a314</uuid>
<source>mod_sofia</source>
<context>default</context>
<chan_name>sofia/internal/1160@192.168.50.229</chan_name>
<originatee>
<originatee_caller_profile>
<username>1160</username>
<dialplan>XML</dialplan>
<caller_id_name>Michael S Collins</caller_id_name>
<ani></ani>
<aniii></aniii>
<caller_id_number>5597993757</caller_id_number>
<network_addr>192.168.50.21</network_addr>
<rdnis></rdnis>
<destination_number>1/A/2137991400</destination_number>
<uuid>e012ce96-7f9c-4194-87fe-5ba86a3930ca</uuid>
<source>mod_sofia</source>
<context>default</context>
<chan_name>OpenZAP/1:23/2137991400</chan_name>
</originatee_caller_profile>
</originatee>
</caller_profile>
<times>
<created_time>1217529338212616</created_time>
<profile_created_time>1217529338212616</profile_created_time>
<progress_time>1217529338450571</progress_time>
<progress_media_time>1217529338452698</progress_media_time>
<answered_time>1217529341528422</answered_time>
<hangup_time>1217529377795951</hangup_time>
<transfer_time>0</transfer_time>
</times>
</callflow> </cdr>
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
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
apt-get install libcurl3-dev libcurl3
make install
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:
<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>
- 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 isapplication/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.