Asterisk 1.6.2.X, cdr_adaptive y calidad de llamadas

Un cliente llama y se queja de la calidad de las llamadas “escucho entrecortado”, “tengo eco” y un largo etcétera. ¿Cómo podemos darle una respuesta fiable y anexarle un reporte de la calidad de sus llamadas? Trabajando con el modulo cdr_adaptive de Asterisk y creando una tabla personalizada donde guardar los datos que nos interesa.

Prerrequisitos: Haber compilado Asterisk con el soporte ODBC

El modulo cdr_adaptive permite crear tablas personalizadas de CDR y guardar los datos en una base de datos a través del conector ODBC. Pero ¿Qué datos vamos a guardar? En esto nos ayuda la función CHANNEL de Asterisk. Entre los distintos valores que podemos leer de un canal activo, están:

  • peerip: la IP de origen del canal
  • rtpqos: permite leer distintas informaciones acerca de la calidad de la llamada. Entre ellas:
    • local_lostpackets: los paquetes audio/video perdidos a lo largo de la llamada
    • local_count: numero total de paquetes audio/video recibidos
    • local_jitter: Jitter local de la llamada
    • remote_lostpackets: los paquetes audio/video perdidos entre los trasmitidos
    • remote_count: numero total de paquetes audio/video trasmitidos
    • remote_jitter: jitter remoto para la llamada
  • useragent: el dispositivo utilizado para la llamada (telefono SIP; Softphone; PBX, etc)
  • audioreadformat: codec audio del canal
  • audiowriteformat: codec audio saliente

Además de estos valores, vamos a escribir en la tabla otros:

  • accountcode: el código o cuenta del cliente
  • hangupcause: variable generada por la aplicación Dial que contiene un código que nos dice como y porque ha terminado la llamada
  • calldate: la fecha de la llamada
  • dst: el numero de destino de la llamada
  • billsec: duración de la llamada desde que ha sido contestada

Algunos de estos campos los escribe Asterisk en automático ya que son parte de las variables predefinidas del CDR (Call Data Record). Otros hay que declararlos en el dialplan. Con estos datos vamos a crear la base de datos (o utilizar una existente) y la tabla correspondiente.

mysql -u root -ppassword

mysql> create database asterisk;

mysql> use asterisk

mysql>  CREATE TABLE `cdr2` (
  `id` bigint(20) NOT NULL auto_increment,
  `calldate` datetime NOT NULL default '0000-00-00 00:00:00',
  `accountcode` varchar(20) default NULL,
  `dst` varchar(80) NOT NULL default '0',
  `billsec` varchar(80) NOT NULL default '0',
  `peerip` varchar(80) NOT NULL default '',
  `useragent` varchar(80) NOT NULL default '',
  `codec1` varchar(80) NOT NULL default '',
  `codec2` varchar(80) NOT NULL default '',
  `tlp` int(15) NOT NULL default '0',
  `llp` int(15) NOT NULL default '0',
  `porlp` decimal(6,3) unsigned zerofill NOT NULL default '000.000',
  `trp` int(15) NOT NULL default '0',
  `rlp` int(15) NOT NULL default '0',
  `porrp` decimal(6,3) unsigned zerofill NOT NULL default '000.000',
  `ljitter` varchar(20) NOT NULL default '',
  `rjitter` varchar(32) NOT NULL default '',
   `hangupcause` varchar(80) NOT NULL default '',
   PRIMARY KEY  (`id`)
   );

Si se fijan hay dos campos de que no hemos hablado:

  • porlp: este campo contendrá el porcentaje de paquetes locales perdidos cuyo calculo se hará a través de una operación matemática muy sencilla
  • porrp: este campo contendrá el porcentaje de paquetes remotos perdidos cuyo calculo se hará a través de una operación matemática muy sencilla

Ahora creamos los permisos para la base de datos asterisk, desde local:

mysql> GRANT ALL PRIVILEGES ON asterisk.* TO 'asterisk'@'localhost' IDENTIFIED BY 'sesamo';

desde remoto:

mysql> GRANT ALL PRIVILEGES ON asterisk.* TO 'asterisk'@'%' IDENTIFIED BY 'sesamo';

actualizamos los permisos:

mysql> flush privileges;

salimos del cliente MySQL:

mysql> quit

Ahora creamos una conexión a la base de datos MySQL con ODBC. Si es la primera vez que utilizan ODBC hay que seguir estos pasos:

nano /etc/odbcinst.ini

Para CentOS 5.7 32 bit debe contener:

[MySQL]
Description = ODBC for MySQL
Driver = /usr/lib/libmyodbc3.so
Setup = /usr/lib/libodbcmyS.so
FileUsage = 1

nano /odbc.ini

[cdr2]
Description = MySQL CDR2
Driver = MySQL
Database = asterisk
Server = localhost
User = asterisk
Password = sesamo
Port = 3306
Option = 3

El bloque de arriba crea una conexión a la base de datos MySQL “asterisk” utilizando como usuario asterisk y como contraseña sesamo. Terminada la configuración de ODBC falta la parte Asterisk donde hay que modificar tres archivos:

  • res_odbc.conf
  • cdr_adaptive_odbc.conf
  • extensions.conf

El primer archivo es él que normalmente se utiliza para la configuración de Asterisk Realtime. Se abre:

nano /etc/asterisk/res_odbc.conf

y al final del archivo se copian las siguientes líneas:

[cdr2]
enabled => yes
dsn => cdr2
username => asterisk
password => sesamo
pre-connect => yes
sanitysql => select 1
idlecheck => 3600
connect_timeout => 10

En dsn hay que poner el nombre de la etiqueta que define el bloque creado en odbc.ini. Se guardan los cambios y se continua con cdr_adaptive.conf

nano /etc/asterisk/cdr_adaptive_odbc.conf

al final del archivo se pone;

[cdr2]
connection=cdr2
table=cdr2
alias start => calldate

los datos:

  • [cdr2]: un nombre que se le asigna a la conexión
  • connection=cdr2: en este parámetro hay que poner la etiqueta que define el bloque creado en res_odbc.conf
  • table=cdr2: la tabla MySQL donde se guardarán los datos
  • alias start => calldate: en cdr_adaptive hay tres campos donde se guardan diferentes momentos de la llamada: cuando inicia, cuando se contesta y cuando se termina. Los nombres de estos  campos son respectivamente start, answer y end. El campo calldate no existe. Por este motivo para que este campo contenga un valor hay que escoger unos de los tres que crea cdr_adaptive. Esto se hace indicando un alias y definiendo que el campo calldate contendrá el valor del campo start de cdr_adaptive (cuando la llamada inicia).

Se guardan los cambios y se pasa al dialplan. Si queremos monitorear todas las llamadas salientes y para ellas utilizamos un proveedor SIP, en el contexto correspondiente ponemos:

[salientes]
exten => _00.,1,Noop(llamadas salientes)
same => n,Dial(SIP/ProveedorSIP/${FILTER(0-9,${EXTEN})})
same => n,Hangup()
exten => h,1,set(CDR(hangupcause)=${HANGUPCAUSE})
exten => h,n,set(CDR(accountcode)=${CDR(accountcode)})
exten => h,n,set(CDR(peerip)=${CHANNEL(peerip)})
exten => h,n,set(CDR(useragent)=${CHANNEL(useragent)})
exten => h,n,set(CDR(codec1)=${CHANNEL(audioreadformat)})
exten => h,n,set(CDR(codec2)=${CHANNEL(audiowriteformat)})
exten => h,n,set(CDR(tlp)=${CHANNEL(rtpqos,audio,local_count)})
exten => h,n,set(CDR(llp)=${CHANNEL(rtpqos,audio,local_lostpackets)})
exten => h,n,set(CDR(porlp)=$[{CHANNEL(rtpqos,audio,local_lostpackets)} / ${CHANNEL(rtpqos,audio,local_count)} * 100])
exten => h,n,set(CDR(trp)=${CHANNEL(rtpqos,audio,remote_count)})
exten => h,n,set(CDR(rlp)=${CHANNEL(rtpqos,audio,remote_lostpackets)})
exten => h,n,set(CDR(porrp)=$[{CHANNEL(rtpqos,audio,remote_lostpackets)} / ${CHANNEL(rtpqos,audio,remote_count)} * 100])
exten => h,n,set(CDR(ljitter)=${CHANNEL(rtpqos,audio,local_jitter)})
exten => h,n,set(CDR(rjitter)=${CHANNEL(rtpqos,audio,remote_jitter)})
exten => h,n,Hangup

Como Asterisk guarda los datos en el CDR cuando finaliza la llamada, podemos utilizar la extensión h para los nuestros. Si se fijan en porlp ý porrp la operación matemática es muy sencilla: se multiplican los paquetes perdidos por los paquetes totales y el resultado se divide por 100. De esta forma se saca el porcentaje de paquetes perdidos.

Se sacan unas cuantas llamadas y se mira que pasa en la tabla CDR2:

mysql -u root -ppassword

mysql> use asterisk

mysql> select calldate,useragent,codec1,codec2,tlp,llp,porlp,trp,rlp,porrp from cdr2;

Captura

mysql> quit

Ahora que tenemos los datos guardados en la base de datos, podemos crear todos los reportes que queramos. Yo lo hago con OpenOffice porque soy perezoso.

Próximamente hablaremos de Zabbix.

Vota el Articulo: 

Sin votos (todavía)
Evalúa la calidad del articulo

10 comentarios

consulta sobre dialplan

hola estimado, excelente tu publicacion. Tengo unas preguntitas, ojala pudieras ayudarme.
Sirve para Asterisk 1.4.x?
En caso de utilizar esto en un server con Asterisk y A2billing, como se deberia agregar este dialplan al contexto del A2billing ???

[a2billing]
exten => _X.,1,DeadAGI,a2billing.php
exten => _X.,2,Wait,2
exten => _X.,3,Hangup

Gracias de antemano.
Un abrazo

Re: consulta sobre dialplan

Hola,

yo lo he probado solamente con Asterisk 1.6 y 1.8

Mira si la función CHANNEL de Asterisk 1.4 permite recolectar esos valores:

CLI> core sho function CHANNEL

Para A2Billing no cambia mucho:

[a2billing]
exten => _X.,1,DeadAGI,a2billing.php
exten => _X.,2,Wait,2
exten => _X.,3,Hangup

exten => h,1,set(CDR(hangupcause)=${HANGUPCAUSE})
exten => h,n,set(CDR(accountcode)=${CDR(accountcode)})
exten => h,n,set(CDR(peerip)=${CHANNEL(peerip)})
exten => h,n,set(CDR(useragent)=${CHANNEL(useragent)})
exten => h,n,set(CDR(codec1)=${CHANNEL(audioreadformat)})
exten => h,n,set(CDR(codec2)=${CHANNEL(audiowriteformat)})
exten => h,n,set(CDR(tlp)=${CHANNEL(rtpqos,audio,local_count)})
exten => h,n,set(CDR(llp)=${CHANNEL(rtpqos,audio,local_lostpackets)})
exten =>
h,n,set(CDR(porlp)=$[{CHANNEL(rtpqos,audio,local_lostpackets)} /
${CHANNEL(rtpqos,audio,local_count)} * 100])
exten => h,n,set(CDR(trp)=${CHANNEL(rtpqos,audio,remote_count)})
exten => h,n,set(CDR(rlp)=${CHANNEL(rtpqos,audio,remote_lostpackets)})
exten =>
h,n,set(CDR(porrp)=$[{CHANNEL(rtpqos,audio,remote_lostpackets)} /
${CHANNEL(rtpqos,audio,remote_count)} * 100])
exten => h,n,set(CDR(ljitter)=${CHANNEL(rtpqos,audio,local_jitter)})
exten => h,n,set(CDR(rjitter)=${CHANNEL(rtpqos,audio,remote_jitter)})
exten => h,n,Hangup

y listo.

Saludos

algo hice mal

Hola Andrea nuevamente. Aplique esto en un asterisk 1.8 y solo me guarda el campo calldate, accountcode,dst y billsec, y desde ahi no guarda mas datos. Que estare haciendo mal ???
es un centos 6.5 64bit con un asterisk 1.8.25 compilado con mysql y odbc.
Muchas gracias

me autorespondo

hola andrea. Me autorespondo. El tema es que en el extension use el dial de tu manual:
[a2billing]
exten => _X.,1,NoOp(A2Billing Start)
same => n,Agi(a2billing.php,1)
same => n,Hangup
exten => h,1,Hangup

entonces al copiar a continuacion la variables para el CDR2, el problema es que aprovechabas la extension h.
Y si te fijas arriba tu cortas h. Por lo que no se alcanzaba a ejecutar. Saque esta linea:
exten => h,1,Hangup
y puse a continuacion el dial para el cdr2 y grabo sin problemas.
Gracias amigo.

otro problemita

Hola estimado, me percate que unos campos no los estaba guardando. El CLI del asterisk me dice lo siguiente:
[Feb 19 23:15:19] WARNING[5636]: sip/dialplan_functions.c:225 sip_acf_channel_read: Unrecognized argument 'rtpqos,audio,local_count' to CHANNEL
[Feb 19 23:15:19] WARNING[5636]: func_channel.c:446 func_channel_read: Unknown or unavailable item requested: 'rtpqos,audio,local_count'
-- Executing [h@a2billing2:7] Set("SIP/9179540779-0000000a", "CDR(tlp)=") in new stack
[Feb 19 23:15:19] WARNING[5636]: sip/dialplan_functions.c:225 sip_acf_channel_read: Unrecognized argument 'rtpqos,audio,local_lostpackets' to CHANNEL
[Feb 19 23:15:19] WARNING[5636]: func_channel.c:446 func_channel_read: Unknown or unavailable item requested: 'rtpqos,audio,local_lostpackets'

por lo que ningun campo que necesite "rtpqos", me guarda informacion. Me da ese warning y no guarda nada.
Sera la version del Asterisk ???? 1.8,25

Re: Otro problemita

Hola,

en la 1.8.25, que es la que uso yo también, las variables cambian de la siguiente forma.

exten => h,1,set(CDR(hangupcause)=${HANGUPCAUSE})
exten => h,n,set(CDR(accountcode)=${CDR(accountcode)})
exten => h,n,set(CDR(peerip)=${uri})
exten => h,n,set(CDR(useragent)=${CHANNEL(useragent)})
exten => h,n,set(CDR(codec1)=${CHANNEL(audioreadformat)})
exten => h,n,set(CDR(codec2)=${CHANNEL(audiowriteformat)})
exten => h,n,set(CDR(tlp)=${CHANNEL(rtpqos,audio,txcount)})
exten => h,n,set(CDR(llp)=${CHANNEL(rtpqos,audio,txploss)})
exten => h,n,set(CDR(porlp)=$[{CHANNEL(rtpqos,audio,txploss)} / ${CHANNEL(rtpqos,audio,txcount)} * 100])
exten => h,n,set(CDR(trp)=${CHANNEL(rtpqos,audio,rxcount)})
exten => h,n,set(CDR(rlp)=${CHANNEL(rtpqos,audio,rxploss)})
exten => h,n,set(CDR(porrp)=$[{CHANNEL(rtpqos,audio,rxploss)} / ${CHANNEL(rtpqos,audio,rxcount)} * 100])
exten => h,n,set(CDR(ljitter)=${CHANNEL(rtpqos,audio,local_maxjitter)})
exten => h,n,set(CDR(rjitter)=${CHANNEL(rtpqos,audio,remote_maxjitter)})
exten => h,n,Hangup

Me cuentas

Saludos

toma los valores pero no los guarda

Hola estimado, hice lo que me dijiste pero sigue sin guardar los valores en la DB, a pesar de que ahora si estan bien los nombres de las variables.
Estaba buscando una herramienta que fuera opensource y sirviera para monitorear las llamadas, y finalmente encontre voipmonitor. No estoy seguro aun si es opensource 100%. La conoces?? tienes alguna opinion sobre ella si es que la conoces????
Un abrazo

Grabar la info en otro servidor

Hola Andrea

Instale y configure el sistema como lo describes y funciona perfectamente

Es posible grabar esta información en otro server?

O sea generar la informacion en el Server A y guardar la Informacion en un Server B con un IP Publico

Saludos y Gracias por la información

Saludos

Re: Grabar la info en otro servidor

Hola Luis,

si es posible. Basta modificar:

[cdr2]
Description = MySQL CDR2
Driver = MySQL
Database = asterisk
Server = localhost
User = asterisk
Password = sesamo
Port = 3306
Option = 3

y en server poner la IP  publica del servidor donde se ha creado la base de datos asterisk con la tabla cdr2. Lo importante es:

  • mysql> GRANT ALL PRIVILEGES ON asterisk.* TO 'asterisk'@'1.2.3.4' IDENTIFIED BY 'sesamo';

Cambiar 1.2.3.4 con la IP publica del servidor donde se encuentra el Asterisk

luego en IPtables dar acceso al servidor MySQL presente en el servidor donde se recolectan los datos solamente a la IP publica del servidor Asterisk.

Me comentas.

Saludos

Suscribirse a Comentarios de "Asterisk 1.6.2.X, cdr_adaptive y calidad de llamadas" Suscribirse a VozToVoice - Todos los comentarios