Asterisk Call Center Stats

Интерфейс статистики очереди - Queue Stats. Прямой лог в нативную БД приложения app_queue. Описание, установка и настройка. Модификация Asternic Call Center Stats lite.

Требования

PHP >= 5.4

Ключевые изменения:

  1. Вместо парсинга текстового queue_log, используется нативный реалтайм лог приложения app_queue в MySQL.
  2. MySQLi, оптимизированы запросы к БД.
  3. Отдельные таблицы агентов и очередей с синхронизацией по требованию.
  4. Новые чарты - Google Charts, вместо flash.
  5. Почасовая выборка.
  6. Детализация принятых и пропущенных вызовов.
  7. Отображение записей разговоров, custom или FreePBX совместимые.
  8. Поиск по Uniqueid, CallerID и агенту в принятых, пропущенных вызовах.
  9. Русский и английский интерфейс.

Описание Asterisk Call Center Stats

Сортировка

  • Выборка данных по: очередям, агентам, дате и времени.

Отвеченные вызовы

  • Отвеченные вызовы по очередям и агентам.
  • Среднее время ожидания для очередей и агентов.
  • Среднее время разговора для очередей и агентов.
  • Количество отвеченных вызовов по агентам.
  • Процент отвеченных вызовов для очередей и агентов.

Неотвеченные вызовы/Поиск по "сырой" базе данных

  • Количество неотвеченных вызовов по очередям.
  • Средняя позиция в очереди при выходе.
  • Причина разъединения: повесили трубку или отключены по таймауту.
  • Распределение неотвеченных вызовов по очередям и период выхода.

Установка Asterisk Call Center Stats

Для установки Asterisk Call Center Stats вам потребуется:

  • Включить запись лога очередей в БД MySQL через ODBC. (подробнее: Asterisk: queue_log в MySQL через unixODBC)
  • Задать имя пользователя и пароль базы данных в конфиге Asterisk Call Center Stats.

Включим лог очереди Asterisk в БД MySQL

По умолчанию данные статистики очереди сохраняются в текстовый лог var/log/asterisk/queue_log. Назначим для хранения данных таблицу БД MySQL - asterisk.queuelog

CREATE TABLE IF NOT EXISTS `queuelog` (
`id` INT NOT NULL AUTO_INCREMENT,
`time` char(32) DEFAULT NULL,
`callid` char(64) DEFAULT NULL,
`queuename` char(64) DEFAULT NULL,
`agent` char(64) DEFAULT NULL,
`event` char(32) DEFAULT NULL,
`data` char(64) DEFAULT NULL,
`data1` char(64) DEFAULT NULL,
`data2` char(64) DEFAULT NULL,
`data3` char(64) DEFAULT NULL,
`data4` char(64) DEFAULT NULL,
`data5` char(64) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


CREATE TABLE IF NOT EXISTS `agents_new` (
`id` MEDIUMINT NOT NULL AUTO_INCREMENT,
`agent` char(64) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `queues_new` (
`id` MEDIUMINT NOT NULL AUTO_INCREMENT,
`queuename` char(64) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

В файле /etc/asterisk/logger.conf (logger_general_custom.conf для FreePBX) выключим лог в файл, если он больше не нужен, но можно вести, как текстовый лог, так и реалтайм лог, одновременно:

 queue_log_to_file = no

В файле /etc/asterisk/asterisk.conf добавив в раздел options:

 [options]
 queue_adaptive_realtime = no

В файле настроек реалтайм /etc/asterisk/extconfig.conf определим драйвер, family и таблицу назначения:

 [settings]
 queue_log => odbc,asterisk,queuelog

где asterisk - db конфиг, например в /etc/asterisk/res_odbc_custom.conf

Если файл extconfig.conf не существует, создайте его командой:

 touch /etc/asterisk/extconfig.conf

и задайте права:

 chown asterisk. /etc/asterisk/extconfig.conf

/etc/asterisk/res_odbc_custom.conf

[asterisk]
enabled => yes
dsn => asterisk
username => dbuser
password => dbpass
pre-connect => yes

Примените конфигурацию

 asterisk -rx 'core reload'

Проверьте соединение Asterisk c базой данных через odbc:

[root@asterisk]# asterisk -rx 'odbc show'

ODBC DSN Settings
-----------------

  Name:   asterisk
  DSN:    qs-asterisk
    Last connection attempt: 1970-01-01 06:00:00
    Number of active connections: 1 (out of 1)

где DSN, настройки из файла /etc/odbc.ini

Скачать Asterisk Call Center Stats


queue-stats-freepbx-rec.tar.gz - Отображение записей из FreePBX

queue-stats-freepbx-rec.tar.gz - Отображение записей из FreePBX

Для отображения записей в формате wav из FreePBX, добавьте следующий контекст в extensions_override_freepbx.conf
и выполните dialplan reload.

[sub-record-check]
exten => s,1,GotoIf($[${LEN(${FROMEXTEN})}]?initialized)
exten => s,n,Set(__REC_STATUS=INITIALIZED)
exten => s,n,Set(NOW=${EPOCH})
exten => s,n,Set(__DAY=${STRFTIME(${NOW},,%d)})
exten => s,n,Set(__MONTH=${STRFTIME(${NOW},,%m)})
exten => s,n,Set(__YEAR=${STRFTIME(${NOW},,%Y)})
exten => s,n,Set(__TIMESTR=${YEAR}${MONTH}${DAY}-${STRFTIME(${NOW},,%H%M%S)})
exten => s,n,Set(__TIMESTRFAKE=${YEAR}${MONTH}${DAY}-000000)
exten => s,n,Set(__FROMEXTEN=${IF($[${LEN(${AMPUSER})}]?${AMPUSER}:${IF($[${LEN(${REALCALLERIDNUM})}]?${REALCALLERIDNUM}:unknown)})})
exten => s,n,Set(__MON_FMT=${IF($["${MIXMON_FORMAT}"="wav49"]?WAV:${MIXMON_FORMAT})})
exten => s,n(initialized),Noop(Recordings initialized)
exten => s,n,ExecIf($[!${LEN(${ARG3})}]?Set(ARG3=dontcare))
exten => s,n,Set(REC_POLICY_MODE_SAVE=${REC_POLICY_MODE})
exten => s,n,ExecIf($["${BLINDTRANSFER}${ATTENDEDTRANSFER}" != ""]?Set(REC_STATUS=NO))
exten => s,n(next),GotoIf($[${LEN(${ARG1})}]?checkaction)
exten => s,n(recorderror),Playback(something-terribly-wrong,error)
exten => s,n,Hangup
exten => s,n(checkaction),GotoIf($[${DIALPLAN_EXISTS(sub-record-check,${ARG1})}]?sub-record-check,${ARG1},1)
exten => s,n,Noop(Generic ${ARG1} Recording Check - ${FROMEXTEN} ${ARG2})
exten => s,n,Gosub(recordcheck,1(${ARG3},${ARG1},${ARG2}))
exten => s,n,Return()

exten => recordcheck,1,Noop(Starting recording check against ${ARG1})
exten => recordcheck,n,Goto(${ARG1})
exten => recordcheck,n(dontcare),Return()
exten => recordcheck,n(always),Noop(Detected legacy "always" entry. Mapping to "force")
exten => recordcheck,n(force),Set(__REC_POLICY_MODE=FORCE)
exten => recordcheck,n,GotoIf($["${REC_STATUS}"!="RECORDING"]?startrec)
exten => recordcheck,n,Return()
exten => recordcheck,n(delayed),Noop(Detected legacy "delayed" entry. Mapping to "yes")
exten => recordcheck,n(yes),ExecIf($["${REC_POLICY_MODE}" = "NEVER" | "${REC_POLICY_MODE}" = "NO" | "${REC_STATUS}" = "RECORDING"]?Return())
exten => recordcheck,n,Set(__REC_POLICY_MODE=YES)
exten => recordcheck,n,Goto(startrec)
exten => recordcheck,n(no),Set(__REC_POLICY_MODE=NO)
exten => recordcheck,n,Return()
exten => recordcheck,n(never),Set(__REC_POLICY_MODE=NEVER)
exten => recordcheck,n,Goto(stoprec)
exten => recordcheck,n(startrec),Noop(Starting recording: ${ARG2}, ${ARG3})
exten => recordcheck,n,Set(AUDIOHOOK_INHERIT(MixMonitor)=yes)
;exten => recordcheck,n,Set(__CALLFILENAME=${ARG2}-${ARG3}-${FROMEXTEN}-${TIMESTR}-${UNIQUEID})
exten => recordcheck,n,Set(__CALLFILENAME=${ARG2}-${ARG3}-${FROMEXTEN}-${TIMESTRFAKE}-${UNIQUEID})
exten => recordcheck,n,MixMonitor(${MIXMON_DIR}${YEAR}/${MONTH}/${DAY}/${CALLFILENAME}.${MON_FMT},abi(LOCAL_MIXMON_ID)${MIXMON_BEEP},${MIXMON_POST})
exten => recordcheck,n,Set(__MIXMON_ID=${LOCAL_MIXMON_ID})
exten => recordcheck,n,Set(__RECORD_ID=${CHANNEL(name)})
exten => recordcheck,n,Set(__REC_STATUS=RECORDING)
exten => recordcheck,n,Set(CDR(recordingfile)=${CALLFILENAME}.${MON_FMT})
exten => recordcheck,n,Return()
exten => recordcheck,n(stoprec),Noop(Stopping recording: ${ARG2}, ${ARG3})
exten => recordcheck,n,Set(__REC_STATUS=STOPPED)
exten => recordcheck,n,System(${AMPBIN}/stoprecording.php "${CHANNEL(name)}")
exten => recordcheck,n,Return()

exten => out,1,Noop(Outbound Recording Check from ${FROMEXTEN} to ${ARG2})
exten => out,n,Set(RECMODE=${DB(AMPUSER/${FROMEXTEN}/recording/out/external)})
exten => out,n,ExecIf($[!${LEN(${RECMODE})} | "${RECMODE}" = "dontcare"]?Goto(routewins))
exten => out,n,ExecIf($["${ARG3}" = "never" | "${ARG3}" = "force"]?Goto(routewins))
exten => out,n(extenwins),Gosub(recordcheck,1(${RECMODE},out,${ARG2}))
exten => out,n,Return()
exten => out,n(routewins),Gosub(recordcheck,1(${ARG3},out,${ARG2}))
exten => out,n,Return()

exten => in,1,Noop(Inbound Recording Check to ${ARG2})
exten => in,n,Set(FROMEXTEN=unknown)
exten => in,n,ExecIf($[${LEN(${CALLERID(num)})}]?Set(FROMEXTEN=${CALLERID(num)}))
exten => in,n,Gosub(recordcheck,1(${ARG3},in,${ARG2}))
exten => in,n,Return()

exten => exten,1,Noop(Exten Recording Check between ${FROMEXTEN} and ${ARG2})
exten => exten,n,Set(CALLTYPE=${IF($[${LEN(${FROM_DID})}]?external:internal)})
exten => exten,n,ExecIf(${LEN(${CALLTYPE_OVERRIDE})}?Set(CALLTYPE=${CALLTYPE_OVERRIDE}))
exten => exten,n,Set(CALLEE=${DB(AMPUSER/${ARG2}/recording/in/${CALLTYPE})})
exten => exten,n,ExecIf($[!${LEN(${CALLEE})}]?Set(CALLEE=dontcare))
exten => exten,n,GotoIf($["${CALLTYPE}"="external"]?callee)
exten => exten,n,GotoIf($["${CALLEE}"="dontcare"]?caller)
exten => exten,n,ExecIf($[${LEN(${DB(AMPUSER/${FROMEXTEN}/recording/priority)})}]?Set(CALLER_PRI=${DB(AMPUSER/${FROMEXTEN}/recording/priority)}):Set(CALLER_PRI=0))
exten => exten,n,ExecIf($[${LEN(${DB(AMPUSER/${ARG2}/recording/priority)})}]?Set(CALLEE_PRI=${DB(AMPUSER/${ARG2}/recording/priority)}):Set(CALLEE_PRI=0))
exten => exten,n,GotoIf($["${CALLER_PRI}"="${CALLEE_PRI}"]?${REC_POLICY}:${IF($[${CALLER_PRI}>${CALLEE_PRI}]?caller:callee)})
exten => exten,n(callee),Gosub(recordcheck,1(${CALLEE},${CALLTYPE},${ARG2}))
exten => exten,n,Return()
exten => exten,n(caller),Set(RECMODE=${DB(AMPUSER/${FROMEXTEN}/recording/out/internal)})
exten => exten,n,ExecIf($[!${LEN(${RECMODE})}]?Set(RECMODE=dontcare))
exten => exten,n,ExecIf($["${RECMODE}"="dontcare"]?Set(RECMODE=${CALLEE}))
exten => exten,n,Gosub(recordcheck,1(${RECMODE},${CALLTYPE},${ARG2}))
exten => exten,n,Return()

exten => conf,1,Noop(Conference Recording Check ${FROMEXTEN} to ${ARG2})
exten => conf,n,Gosub(recconf,1(${ARG2},${ARG2},${ARG3}))
exten => conf,n,Return()

exten => page,1,Noop(Paging Recording Check ${FROMEXTEN} to ${ARG2})
exten => page,n,GosubIf($["${REC_POLICY_MODE}"="always"]?recconf,1(${ARG2},${FROMEXTEN},${ARG3}))
exten => page,n,Return()

exten => recconf,1,Noop(Setting up recording: ${ARG1}, ${ARG2}, ${ARG3})
exten => recconf,n,Set(__CALLFILENAME=${IF($[${CONFBRIDGE_INFO(parties,${ARG2})}]?${DB(RECCONF/${ARG2})}:${ARG1}-${ARG2}-${ARG3}-${TIMESTR}-${UNIQUEID})})
exten => recconf,n,ExecIf($[!${CONFBRIDGE_INFO(parties,${ARG2})}]?Set(DB(RECCONF/${ARG2})=${CALLFILENAME}))
exten => recconf,n,Set(CONFBRIDGE(bridge,record_file)=${MIXMON_DIR}${YEAR}/${MONTH}/${DAY}/${CALLFILENAME}.${MON_FMT})
exten => recconf,n,ExecIf($["${ARG3}"!="always"]?Return())
exten => recconf,n,Set(CONFBRIDGE(bridge,record_conference)=yes)
exten => recconf,n,Set(__REC_STATUS=RECORDING)
exten => recconf,n,Set(CDR(recordingfile)=${IF($[${CONFBRIDGE_INFO(parties,${ARG2})}]?${CALLFILENAME}.${MON_FMT}:${CALLFILENAME}.${MON_FMT})})
exten => recconf,n,Noop(${MIXMONITOR_FILENAME})
exten => recconf,n,Set(CHANNEL(hangup_handler_push)=sub-record-hh-check,s,1)
exten => recconf,n,Return()

exten => recq,1,Noop(Setting up recording: ${ARG1}, ${ARG2}, ${ARG3})
exten => recq,n,Set(AUDIOHOOK_INHERIT(MixMonitor)=yes)
exten => recq,n,Set(MONITOR_FILENAME=${MIXMON_DIR}${YEAR}/${MONTH}/${DAY}/${CALLFILENAME})
exten => recq,n,MixMonitor(${MONITOR_FILENAME}.${MON_FMT},${MONITOR_OPTIONS}${MIXMON_BEEP},${MIXMON_POST})
exten => recq,n,Set(__REC_STATUS=RECORDING)
exten => recq,n,Set(CDR(recordingfile)=${CALLFILENAME}.${MON_FMT})
exten => recq,n,Return()

exten => parking,1,Noop(User ${ARG2} picked up a parked call)
exten => parking,n,Set(USER=${ARG2})
exten => parking,n,ExecIf($[!${LEN(${ARG2})}]?Set(USER=unknown))
exten => parking,n,Set(RECMODE=${DB(AMPUSER/${ARG2}/recording/out/internal)})
exten => parking,n,ExecIf($[!${LEN(${RECMODE})}]?Set(RECMODE=dontcare))
exten => parking,n,Gosub(recordcheck,1(${RECMODE},parked,${USER}))
exten => parking,n,Return()

авторизация через FreePBX

авторизация через FreePBX

$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];

$mysqli_ast = new mysqli("localhost", "freepbxuser", "freepbxpassword", "asterisk");
$mysqli_ast->set_charset('utf8');

$valid_passwords2 = $mysqli_ast->query("SELECT password_sha1 FROM ampusers WHERE username = '$user'");
$valid_passwords = $valid_passwords2->fetch_row();

//$pass2 = md5($valid_passwords[1].':'.$_SERVER['SERVER_ADDR'].':'.$pass) == $valid_passwords[0]);
$validated = (sha1($pass) == $valid_passwords[0]);

if (!$validated) {
  header('WWW-Authenticate: Basic realm="fs-tst"');
    header('HTTP/1.0 401 Unauthorized');
      die ("Not authorized");
      }
      $valid_passwords2->free();
      $mysqli_ast->close();

Распакуйте:

 cd /var/www/html
 tar zxvf queue-stats-current.tar.gz
 cd queue-stats-current

Конфиг Asterisk Call Cetnter Stats

Отредактируйте файл config.php в соответствии в вашими данными, где

  • $DBServer - хост (localhost)
  • $DBUser - Пользователь БД.
  • $DBPass - Пароль БД.
  • $DBName - Имя БД.
  • $DBTable = - Имя таблицы БД.
// Credentials for MYSQL database
$DBServer = 'localhost';
$DBUser   = 'root';
$DBPass   = '';
$DBName   = 'asterisk';
$DBTable   = 'queuelog';

$connection = new mysqli($DBServer, $DBUser, $DBPass, $DBName);

Задайте права на директорию queue-stats:

 chown -R asterisk. /var/www/html/queue-stats

Откройте статистику в вашем любимом веб браузере:

http://ip.add.res.s/queue-stats

Для добавления агентов и очередей, нажмите соответствующие кнопки:

Только авторизованные участники могут оставлять комментарии.
  • soft/call_center/asternic-call-center-stats.txt
  • Последние изменения: 2018/07/04