Получение кодов отбоя SIP и стороны завершившей вызов в Asterisk
В последних релизах Asterisk есть возможность получить коды отбоя Asterisk или SIP-код отбоя при помощи функции HANGUPCAUSE. В заметке приводится практический пример того как это сделать,а затем записать результаты в БД MySQL. Также приведен пример того, как в Asterisk узнать какая из сторон была инициатором завершения вызова.
В консоли Asterisk посмотрим описание функции HANGUPCAUSE.
CLI> core show function HANGUPCAUSE -= Info about function 'HANGUPCAUSE' =- [Synopsis] Gets per-channel hangupcause information from the channel. [Description] Gets technology-specific or translated Asterisk cause code information from the channel for the specified channel that resulted from a dial. [Syntax] HANGUPCAUSE(channel,type) [Arguments] channel The name of the channel for which to retrieve cause information. type Parameter describing which type of information is requested. Types are: tech - Technology-specific cause information ast - Translated Asterisk cause code [See Also] HANGUPCAUSE_KEYS, HangupCauseClear()
Всё достаточно просто. Но есть нюанс, что каналы, которые участвовали в соединении, могут не иметь коды отбоев.
Поэтому в Asterisk есть специальная функция HANGUPCAUSE_KEYS
, которая содержим имена каналов, содержащие коды отбоя:
*CLI> core show function HANGUPCAUSE_KEYS -= Info about function 'HANGUPCAUSE_KEYS' =- [Synopsis] Gets the list of channels for which hangup causes are available. [Description] Returns a comma-separated list of channel names to be used with the HANGUPCAUSE function. [Syntax] HANGUPCAUSE_KEYS() [Arguments] Not available [See Also] HANGUPCAUSE, HangupCauseClear()
Всё вместе это выглядит так:
h => { if ($[${LEN(${HANGUPCAUSE_STRING})} = 0]) goto stop; NoOp(SIP cause: ${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},tech)}); NoOp(Ast cause: ${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},ast)}); NoOp(Hangupcause keys: ${HANGUPCAUSE_KEYS()}); Set(CDR(userfield)=${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},tech)} | ${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},ast)} | ${HANGUPCAUSE_KEYS()}); stop: NoOp(HANGUPCAUSE is empty); };
В дайлплане удачный звонок на номер 5500:
-- Channel SIP/CCM61-WC-00006071 left 'simple_bridge' basic-bridge <5e218c7f-08e4-402b-a201-9fe95e43645c> -- Channel SIP/AST-CC-ERT-2-00006070 left 'simple_bridge' basic-bridge <5e218c7f-08e4-402b-a201-9fe95e43645c> == Spawn extension (ERT-IN, 5500, 6) exited non-zero on 'SIP/AST-CC-ERT-2-00006070' -- Executing [h@ERT-IN:1] NoOp("SIP/AST-CC-ERT-2-00006070", "SIP cause: SIP 200 OK") in new stack -- Executing [h@ERT-IN:2] NoOp("SIP/AST-CC-ERT-2-00006070", "Ast cause: Normal Clearing") in new stack -- Executing [h@ERT-IN:3] NoOp("SIP/AST-CC-ERT-2-00006070", "Hangupcause keys: SIP/CCM61-WC-00006071") in new stack -- Executing [h@ERT-IN:4] Set("SIP/AST-CC-ERT-2-00006070", "CDR(userfield)=SIP 200 OK | Normal Clearing | SIP/CCM61-WC-00006071") in new stack
То есть в бридже у нас были каналы SIP/CCM61-WC-00006071
и SIP/AST-CC-ERT-2-00006070
, первым брижд покинул Channel SIP/CCM61-WC-00006071
, то есть он является инициатором отбоя, но в переменной HANGUPCAUSE_KEYS() содержится только код отбоя второго канала SIP/AST-CC-ERT-2-00006070
.
Теперь, посмотрим что содержит сама переменная:
-
Translated Asterisk cause code: Normal Clearing
-
Translated Asterisk cause code: SIP 200 OK
Если вызов состоялся, а переменная пустая, то значит ни один канал не содержал кода отбоя.
Возьмём звонок на сервис, то есть по сути с одним плечом SIP, в таком случае переменная HANGUPCAUSE_KEYS()
будет пустая:
[Jan 22 18:17:15] WARNING[13817][C-0000306d]: func_hangupcause.c:140 hangupcause_read: Unable to find information for channel -- Executing [h@WC-IN:1] NoOp("SIP/CCM61-WC-0000608c", "SIP cause: ") in new stack [Jan 22 18:17:15] WARNING[13817][C-0000306d]: func_hangupcause.c:140 hangupcause_read: Unable to find information for channel -- Executing [h@WC-IN:2] NoOp("SIP/CCM61-WC-0000608c", "Ast cause: ") in new stack -- Executing [h@WC-IN:3] NoOp("SIP/CCM61-WC-0000608c", "Hangup keys: ") in new stack [Jan 22 18:17:15] WARNING[13817][C-0000306d]: func_hangupcause.c:140 hangupcause_read: Unable to find information for channel [Jan 22 18:17:15] WARNING[13817][C-0000306d]: func_hangupcause.c:140 hangupcause_read: Unable to find information for channel -- Executing [h@WC-IN:4] Set("SIP/CCM61-WC-0000608c", "CDR(userfield)= | | ") in new stack
Вызов который завершился неудачей:
-- Got SIP response 503 "Service Unavailable" back from 10.101.1.7:5060 -- SIP/ASTERISK-ERT-1-00006090 is circuit-busy == Everyone is busy/congested at this time (1:0/1/0) -- Executing [22000@WC-IN:7] NoOp("SIP/CCM61-WC-0000608f", "CONGESTION") in new stack -- Executing [22000@WC-IN:8] Goto("SIP/CCM61-WC-0000608f", "sw_378_CONGESTION,10") in new stack -- Goto (WC-IN,sw_378_CONGESTION,10) -- Executing [sw_378_CONGESTION@WC-IN:10] Hangup("SIP/CCM61-WC-0000608f", "") in new stack == Spawn extension (WC-IN, sw_378_CONGESTION, 10) exited non-zero on 'SIP/CCM61-WC-0000608f' -- Executing [h@WC-IN:1] NoOp("SIP/CCM61-WC-0000608f", "SIP cause: SIP 503 Service Unavailable") in new stack -- Executing [h@WC-IN:2] NoOp("SIP/CCM61-WC-0000608f", "Ast cause: Circuit/channel congestion") in new stack -- Executing [h@WC-IN:3] NoOp("SIP/CCM61-WC-0000608f", "Hangup keys: SIP/ASTERISK-ERT-1-00006090") in new stack -- Executing [h@WC-IN:4] Set("SIP/CCM61-WC-0000608f", "CDR(userfield)=SIP 503 Service Unavailable | Circuit/channel congestion | SIP/ASTERISK-ERT-1-00006090 ") in new stack
Посмотрим что содержит сама переменная:
-
Hangup keys:
SIP/ASTERISK-ERT-1-00006090
-
Translated Asterisk cause code:
Circuit/channel congestion
-
Translated Asterisk cause code:
SIP 503 Service Unavailable
Я заметил, что если lastapp в CDR != Hangup, а Dial, то поля в CDR которые я пытаюсь обновить CDR(userfield|dialstatus) не обновляются, вот пример с одним uniqueid и две записи:
mysql> select * from cdr where uniqueid=1548253072.2783; +-----+---------------------+----------------------------+--------+-------------+----------+-----------------------+-----------------------------+---------+------------------------------+----------+---------+-------------+----------+-------------+-----------------+------------+--------------------------------------------------------------+ | id | calldate | clid | src | dst | dcontext | channel | dstchannel | lastapp | lastdata | duration | billsec | disposition | amaflags | accountcode | uniqueid | dialstatus | userfield | +-----+---------------------+----------------------------+--------+-------------+----------+-----------------------+-----------------------------+---------+------------------------------+----------+---------+-------------+----------+-------------+-----------------+------------+--------------------------------------------------------------+ | 986 | 2019-01-23 17:17:52 | "Kudr Ignat" <015166> | 015166 | 12345 | WC-IN | SIP/CCM61-WC-00000706 | SIP/ASTERISK-ERT-1-00000707 | Dial | SIP/ASTERISK-ERT-1/12345,,tT | 4 | 0 | BUSY | 3 | | 1548253072.2783 | | | | 987 | 2019-01-23 17:17:56 | "Kudr Ignat" <015166> | 015166 | sw_525_BUSY | WC-IN | SIP/CCM61-WC-00000706 | | Hangup | | 0 | 0 | BUSY | 3 | | 1548253072.2783 | | SIP 486 Busy Here | User busy | SIP/ASTERISK-ERT-1-00000707 | +-----+---------------------+----------------------------+--------+-------------+----------+-----------------------+-----------------------------+---------+------------------------------+----------+---------+-------------+----------+-------------+-----------------+------------+--------------------------------------------------------------+ 2 rows in set (0.01 sec)
Это связано с тем, что вызов хоть и попал на h, но CDR не записался на диск.
Who Hung Up? Asterisk Wiki
По ссылке описывается более сложный пример, в нём мы разбираем пременную HANGUPCAUSE_KEYS(), ведь он в теории может содержать больше имени одного канала, и тогда в моём примере попросту будет ошибка. Но у меня было максимум 1 переменная.
Приведу пример ниже:
[foo] exten => s,1,Dial(SIP/bar) exten => h,1,noop() exten => h,n,set(HANGUPCAUSE_STRING=${HANGUPCAUSE_KEYS()}) ; start loop exten => h,n(hu_begin),noop() ; check exit condition (no more array to check) exten => h,n,gotoif($[${LEN(${HANGUPCAUSE_STRING})} = 0]?hu_exit) ; pull the next item exten => h,n,set(ARRAY(item)=${HANGUPCAUSE_STRING}) exten => h,n,set(HANGUPCAUSE_STRING=${HANGUPCAUSE_STRING:${LEN(${item})}}) ; display the channel ID and cause code exten => h,n,noop(got channel ID ${item} with pvt cause ${HANGUPCAUSE(${item},tech)}) ; check exit condition (no more array to check) exten => h,n,gotoif($[${LEN(${HANGUPCAUSE_STRING})} = 0]?hu_exit) ; we still have entries to process, so strip the leading comma exten => h,n,set(HANGUPCAUSE_STRING=${HANGUPCAUSE_STRING:1}) ; go back to the beginning of the loop exten => h,n,goto(hu_begin) exten => h,n(hu_exit),noop(All HANGUPCAUSE entries processed)
Можно создать свою таблицу CDR или содержащую иную информацию по вкусу и записывать в БД.
В примере ниже, по результатам вызова, в таблицу Mysql c именем cdr_custom записываются нужные переменные CDR плюс коды отбоя, ниже приводится подробный пример.
Предпологается? что мы будем использовать ранее созданное MySQL ODBC соединение.
Все поля, кроме dialstatus и REL будем брать из функции CDR().
Переменная REL формируется из функции HANGUPCAUSE:
Set(REL=${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},tech)} | ${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},ast)} | ${HANGUPCAUSE_KEYS()});
Переменная ${DIALSTATUS}
также стандартная переменная.
${DIALSTATUS}: This is the status of the call CHANUNAVAIL CONGESTION NOANSWER BUSY ANSWER CANCEL DONTCALL: For the Privacy and Screening Modes. Will be set if the called party chooses to send the calling party to the 'Go Away' script. TORTURE: For the Privacy and Screening Modes. Will be set if the called party chooses to send the calling party to the 'torture' script. INVALIDARGS
Ниже строка дайлплана Asterisk:
NoOp(${CDR(uniqueid)},${CDR(start)},${CDR(clid)},${CDR(src)},${CDR(dst)},${CDR(dcontext)},${CDR(channel)},${CDR(dstchannel)},${CDR(duration)},${CDR(billsec)},${CDR(disposition)},${DIALSTATUS},${REL})
В консоли Asterisk строка выше с подставленными переменными будет выглядеть так:
1548251202.2649,2019-01-23 16:46:42,"79210001122" <79210001122>,79210001122,600,WC-IN,SIP/CCM61-WC-000006aa,SIP/AST-CC-ERT-1-000006ab,409,409,ANSWERED,ANSWER,SIP 200 OK | Normal Clearing
Теперь все эти переменные нужно записать подготовленную для этих целей таблицу MySQL при помощи функции ODBC которая будет вызаться из дайлплана на экстеншене h.
Создадим новую таблицу cdr_custom в БД MySQL
mysql> use ertx_ast_cdr; mysql> CREATE TABLE cdr_custom ( id int(11) unsigned NOT NULL auto_increment PRIMARY KEY, uniqueid varchar(32) NOT NULL default '', calldate datetime NOT NULL default '0000-00-00 00:00:00', clid varchar(80) NOT NULL default '', src varchar(80) NOT NULL default '', dst varchar(80) NOT NULL default '', dcontext varchar(80) NOT NULL default '', channel varchar(80) NOT NULL default '', dstchannel varchar(80) NOT NULL default '', duration int(11) NOT NULL default '0', billsec int(11) NOT NULL default '0', disposition varchar(45) NOT NULL default '', dialstatus varchar(17) NOT NULL default '' ,cause varchar(120) NOT NULL default '' ); mysql> desc cdr_custom; +-------------+------------------+------+-----+---------------------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+------------------+------+-----+---------------------+----------------+ | id | int(11) unsigned | NO | PRI | NULL | auto_increment | | uniqueid | varchar(32) | NO | | | | | calldate | datetime | NO | | 0000-00-00 00:00:00 | | | clid | varchar(80) | NO | | | | | src | varchar(80) | NO | | | | | dst | varchar(80) | NO | | | | | dcontext | varchar(80) | NO | | | | | channel | varchar(80) | NO | | | | | dstchannel | varchar(80) | NO | | | | | duration | int(11) | NO | | 0 | | | billsec | int(11) | NO | | 0 | | | disposition | varchar(45) | NO | | | | | dialstatus | varchar(17) | NO | | | | | cause | varchar(120) | NO | | | | +-------------+------------------+------+-----+---------------------+----------------+ 14 rows in set (0.00 sec)
Теперь создадим функцию в /etc/asterisk/func_odbc.conf
[INSERT_CUSTOM_CDR] dsn=asterisk writesql=INSERT INTO cdr_custom (uniqueid,calldate,clid,src,dst,dcontext,channel,dstchannel,duration,billsec,disposition,dialstatus,cause) VALUES ('${ARG1}','${ARG2}','${ARG3}','${ARG4}','${ARG5}','${ARG6}','${ARG7}','${ARG8}','${ARG9}','${ARG10}','${ARG11}','${ARG12}','${ARG13}')
Настала очередь dialplan.
Ловим вызова на специальном экстеншене h куда попадает вызов после заверешния:
>h => { Set(REL=${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},tech)} | ${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},ast)} | ${HANGUPCAUSE_KEYS()}); Set(ODBC_INSERT_CUSTOM_CDR(${CDR(uniqueid)},${CDR(start)},${CDR(clid)},${CDR(src)},${CDR(dst)},${CDR(dcontext)},${CDR(channel)},${CDR(dstchannel)},${CDR(duration)},${CDR(billsec)},${CDR(disposition)},${DIALSTATUS},${REL})=); {
Причём обращаю внимание, что для записи значений из dialplan в ODBC нужно использовать SET!!!
Если в дайлплане используется case
Тогда в CDR в столбце dst может быть вместо Б-номера может быть значение sw_564_CHANUNAVAIL
или что-то похожее вместо Б-номера:
mysql> select * from ertx_ast_cdr.cdr_custom where src in ('49000','015136') and calldate >= '2019-03-06 09:00:00'; +-------+------------------+---------------------+---------------------------+--------+--------------------+----------+-----------------------+-----------------------------+----------+---------+-------------+------------+------------------------------------------------------------------+ | id | uniqueid | calldate | clid | src | dst | dcontext | channel | dstchannel | duration | billsec | disposition | dialstatus | cause | +-------+------------------+---------------------+---------------------------+--------+--------------------+----------+-----------------------+-----------------------------+----------+---------+-------------+------------+------------------------------------------------------------------+ | 27633 | 1551942737.86893 | 2019-03-07 10:12:17 | Kudryavcev Ignat <015136> | 015136 | 450000 | WC-IN | SIP/CCM61-WC-0000de97 | SIP/ASTERISK-ERT-1-0000de98 | 2 | 0 | NO ANSWER | CANCEL | SIP 180 Ringing | Normal Clearing | SIP/ASTERISK-ERT-1-0000de98 | | 27634 | 1551942783.86900 | 2019-03-07 10:13:03 | Kudryavcev Ignat <015136> | 015136 | sw_564_CHANUNAVAIL | WC-IN | SIP/CCM61-WC-0000de9d | | 0 | 0 | CONGESTION | CONGESTION | | | SIP/ASTERISK-ERT-2-0000de9f | | 27635 | 1551942795.86906 | 2019-03-07 10:13:15 | Kudryavcev Ignat <015136> | 015136 | 450000 | WC-IN | SIP/CCM61-WC-0000dea0 | SIP/ASTERISK-ERT-1-0000dea1 | 2 | 0 | NO ANSWER | CANCEL | SIP 180 Ringing | Normal Clearing | SIP/ASTERISK-ERT-1-0000dea1 | | 27636 | 1551942821.86912 | 2019-03-07 10:13:41 | Kudryavcev Ignat <015136> | 015136 | sw_577_BUSY | WC-IN | SIP/CCM61-WC-0000dea5 | | 0 | 0 | BUSY | BUSY | SIP 486 Busy here | User busy | SIP/ASTERISK-ERT-1-0000dea6 |
Ниже пример Dialplan в котором может возникнуть такая ситуация:
_[56]XX => { NoOp(Call from Westcall); //Set(CALLERID(num)=01${CALLERID(num)}); Set(CALLERID(num)=${IF($[${LEN(${CALLERID(num)})} = 4]?01${CALLERID(num)}:${CALLERID(num)})}); Verbose(!!! DEBUG Call from Westcall to ERTEL from ${CALLERID(num)} ${CALLERID(name)} --called-number-> ${EXTEN} !!!); Verbose(!!! ${CALLERID(all)} !!!); Set(_REAL_DST=${EXTEN}); Dial(SIP/${EXTEN}@AST-CC-ERT-1,,tT); //Dial(SIP/5136@10.10.10.10); NoOp(${DIALSTATUS}); switch(${DIALSTATUS}) { case BUSY: Hangup; case ANSWER: Hangup; case NOANSWER: Hangup; case CONGESTION: Hangup; default: Dial(SIP/${EXTEN}@AST-CC-ERT-2,,tT); Hangup;
В данном примере, мы пытаемся перемаршрутизировать вызов на резервный сервер в зависимости от ответа основного.
Поэтому сохраним набранный номер в переменную Set(_REAL_DST=${EXTEN});
Затем при сохранении CDR заменим сохранение dst как ${CDR(dst)}
на ${REAL_DST}
На Asterisk перезагрузить func_odbc.so и dailplan(dialplan reload/ael reload)
voip-gate-aster*CLI> module reload func_odbc.so Module 'func_odbc.so' reloaded successfully. -- Reloading module 'func_odbc.so' (ODBC lookups) == Parsing '/etc/asterisk/func_odbc.conf': Found == Unregistered custom function ODBC_PRESENCE == Unregistered custom function ODBC_ANTIGF == Unregistered custom function ODBC_SQL == Registered custom function 'ODBC_SQL' == Registered custom function 'ODBC_ANTIGF' == Registered custom function 'ODBC_PRESENCE' == Registered custom function 'ODBC_INSERT_CUSTOM_CDR' voip-gate-aster*CLI> odbc write ODBC_ ODBC_ANTIGF ODBC_PRESENCE ODBC_INSERT_CUSTOM_CDR ODBC_SQL
Запустим в dialplan:
-- Executing [h@WC-IN:1] Set("SIP/CCM61-WC-000006fa", "REL=SIP 200 OK | Normal Clearing | SIP/ASTERISK-ERT-1-000006fb") in new stack -- Executing [h@WC-IN:2] Set("SIP/CCM61-WC-000006fa", "ODBC_INSERT_CUSTOM_CDR(1548252647.2763,2019-01-23 17:10:47,"" <015139>,015139,450000,WC-IN,SIP/CCM61-WC-000006fa,SIP/ASTERISK-ERT-1-000006fb,1,1,ANSWERED,ANSWER,SIP 200 OK | Normal Clearing | SIP/ASTERISK-ERT-1-000006fb)=") in new stack
В таблице CDR:
mysql> select * from cdr_custom order by id desc limit 2; +-----+-----------------+---------------------+---------------------------+-------------+-------+----------+---------------------------+-----------------------------+----------+---------+-------------+------------+-------------------------------------------------------------+ | id | uniqueid | calldate | clid | src | dst | dcontext | channel | dstchannel | duration | billsec | disposition | dialstatus | cause | +-----+-----------------+---------------------+---------------------------+-------------+-------+----------+---------------------------+-----------------------------+----------+---------+-------------+------------+-------------------------------------------------------------+ | 230 | 1548253896.2820 | 2019-01-23 17:31:36 | Kudr Ignat <015040> | 015040 | 450000 | WC-IN | SIP/CCM61-WC-0000071c | SIP/ASTERISK-ERT-1-0000071d | 56 | 36 | ANSWERED | ANSWER | SIP 200 OK | Normal Clearing | SIP/ASTERISK-ERT-1-0000071d | | 229 | 1548253402.2799 | 2019-01-23 17:23:22 | 79053332211 <79053332211> | 79053332211 | 5500 | ERT-IN | SIP/AST-CC-ERT-2-0000070e | SIP/CCM61-WC-0000070f | 348 | 348 | ANSWERED | ANSWER | SIP 200 OK | Normal Clearing | SIP/CCM61-WC-0000070f | +-----+-----------------+---------------------+---------------------------+-------------+-------+----------+---------------------------+-----------------------------+----------+---------+-------------+------------+-------------------------------------------------------------+ 2 rows in set (0.00 sec) </span> Как в Asterisk узнать какая из сторон была инициатором отбоя? Пример дайлплана: exten => _1XX,1,Set(HANGUPSIDE=originator) exten => _1XX,2,Dial(SIP/${EXTEN},,g) exten => _1XX,3,Set(HANGUPSIDE=terminator) exten => _1XX,4,NoOp(${HANGUPSIDE}) exten => h,1,NoOp(${HANGUPSIDE})
В первом шаге, мы устанавливам что по умолчания в переменной HANGUPSIDE оригинатор завершит вызов.
На втором шаге мы отправляем вызов терминатору, но указываем, в приложении Dial опцию g.
Что такое опция g описано в Help Asterisk:
g: Proceed with dialplan execution at the next priority in the current extension if the destination channel hangs up.
Без опций, приложение Dial, после отбоя любой из сторон передаёт вызов сразу же на экстеншен h текущего контекста, выполнение следующего шага дайлплана не происходит.
Если поставить опцию g, то следующий шаг дайлплана после Dial будет выполняться если терминирующий канал первым завершил вызов, в данном случае установка переменной HANGUPSIDE равной terminator.
Таким образом легко установить какая из причин является инициатором завершение, что иногда может очень сильно помочь в диагностике проблем.
Автор: Игнат Кудрявцев
Похожие материалы: