В одной из заметок, Забираем звонки из Webitel, я описал, как с помощью REST API вы можете забирать информацию о звонках из Webitel. Сегодня мы рассмотрим на примере Звонки по направлениям, как работать с агрегациями и показателями. Для лучшего понимания примера, настоятельно рекомендую ознакомится с разделом агрегации документации по elasticsearch.
Предположим, что нам нужно рассчитать количество, а так же показатели общей и средней длительности звонков по направлениям за текущие сутки. Тело запроса будет иметь следующий вид:
{ "index": "cdr-a", "limit": 0, "aggs": { "direction": { "terms": { "field": "direction", "order": { "_count": "desc" }, "size": 5 }, "aggs": { "talksec_sum": { "sum": { "field": "talksec" } }, "talksec_avg": { "avg": { "field": "talksec" } } } } }, "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/d", "lte": "now" } } } ] } } ] }
В нашем запросе группировка происходит по полю direction, а среднее и суммарное значение мы вычисляем по полю talksec. Дополнительно мы добавляем фильтр по времени: от текущей даты до начала суток.
Запрос с помощью утилиты cURL будет иметь вид:
curl -s -L -XPOST \ -H 'Content-Type: application/json' \ -H 'X-Access-Token: eyJMDU0NjAwMDAwMCwhaW4iLCJ2IjoyfQ.VeWrCqkv_lG1bLVv6tvOFPz2XfhiTpQG8XcFji8gSS4'\ "https://cloud-eu.webitel.com/engine/api/v2/cdr/text" -d@calls_by_direction.json
В результате мы получим JSON документ с результатом запроса в объекте aggregations, который уже легко можем обработать любимыми инструментами:
{ "took": 45, "timed_out": false, "_shards": { "total": 33, "successful": 33, "skipped": 0, "failed": 0 }, "hits": { "total": 343, "max_score": 0, "hits": [] }, "aggregations": { "direction": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "outbound", "doc_count": 209, "talksec_sum": { "value": 7093 }, "talksec_avg": { "value": 55.4140625 } }, { "key": "inbound", "doc_count": 94, "talksec_sum": { "value": 2430 }, "talksec_avg": { "value": 73.63636363636364 } }, { "key": "internal", "doc_count": 12, "talksec_sum": { "value": 172 }, "talksec_avg": { "value": 24.571428571428573 } } ] } } }
В данной заметке рассмотрим какие ключевые изменения претерпела Kibana
Права доступа
В версии Webitel 3.11 была добавлена новая администрируемая роль kibana в Access Control List. Обратите внимание на то, что если вы обновили ваш сервер с версии 3.10, тогда вам так же необходимо разрешить нужным группам пользователей доступ в Kibana, иначе ваше пользователи не смогут войти в систему аналитики. Теперь у вас есть возможность выставить права на чтение, создание, изменения и удаление визулазаций либо дашбордов.
Пространства / Spaces
Новый функционал, который позволяет группировать настройки индексов, визуализаций и дашбордов. После обновления все ваши старые графики автоматически конвертируются в новый формат и будут доступны в пространстве Default. Так же, обратите внимание на то, что для обеспечения совместимости с ранними версиями данное пространство не администрируется правами доступа. Если у пользователя есть права на чтение, тогда он всегда будет видеть все визуализации в пространстве Default.
Доступ к пространствам
После создания нового пространства, вы можете указать каким группам пользователей какие права предоставлять к данному Space:
Timelion
Новый тип визуализации Timelion, который позволяет создавать time series графики с использование хорошо документированного синтаксиса:
Canvas
Новый подход к созданию интерактивных экранов
Инструменты разработчика
Для отладки запросов в elasticsearch мы добавили инструмент Dev Tools
В новой версии Webitel 3.11 мы добавили двухфакторную аутентификацию для пользователя root, что бы обеспечить дополнительную защиту для ваших сервером. Для того, что бы включить данный функционал, вам нужно в файле env/common включить параметр и перезапустить сервис:
application:auth:useTOTP=true
Следующий шаг - перейти в новый пункт меню Security
И активировать защиту:
После чего у вас не экране появится QR код:
Отсканируйте его с помощью приложения Google Authenticator:
Теперь, после ввода пароля для пользователя root, дополнительно нужно будет вводить одноразовый пароль из мобильного приложения:
Еще в 2014 году в Viber добавили поддержку бесплатных исходящих звонков на номера сети iNum. Что такое iNum? iNum (international Number) - международный номер. Данные номера позиционируются как глобальные телефонные номера, не зависящие от географической привязки, текущего местоположения, расстояния и государственных границ. iNum использует телефонный код +883.
Что нужно для подключения номера +883? Самый простой способ - зарегистрироваться на zadarma или youmagic, либо другого SIP провайдера, который предоставляет такие номера. Подключаем номер +883 к Webitel и теперь клиенты могут бесплатно звонить с Viber к нам:
Наша задача на сегодня - сразу после того, как мы поставим абонента в очередь ожидания, озвучить среднее время до соединения с оператором.
Для решения задачи мы воспользуемся приложением CDR, которое позволит нам выполнить запрос в базу elasticsearch и получить среднее время ожидания абонентов в очереди main за последний час. Правильный запрос в elasticsearch будет иметь вот такой вид:
Остается оформить все в нашей схеме ACR: получить целую часть из среднего значения ожидания в очереди, текст озвучим с помощью синтеза речи и т.д.
Привожу простой пример такой схемы:
[ { "cdr": { "exportVar": { "avg_wait": "aggregations.waiting.value" }, "elastic": { "aggs": { "waiting": { "avg": { "field": "queue.wait_duration" } } }, "index": "cdr-a*", "limit": 0, "query": "*", "filter": { "bool": { "must": [ { "match": { "queue.name": "main" } }, { "range": { "created_time": { "gte": "now-1h", "lte": "now" } } } ] } } } } }, { "math": { "data": "${avg_wait}", "setVar": "avg_wait", "fn": "ceil" } }, { "log": "avg_wait: ${avg_wait}" }, { "queue": { "name": "main", "timer": { "interval": 1, "tries": 1, "actions": [ { "ccPosition": { "var": "ccPosition" } }, { "if": { "expression": "!${avg_wait} || ${avg_wait} < 60", "then": [ { "tts": { "text": "Расчетное время ожидания до соединения с оператором меньше одной минуты.", "voice": "Maxim" } } ] } }, { "if": { "expression": "${avg_wait} > 120", "then": [ { "tts": { "text": "Расчетное время ожидания до соединения с оператором больше двух минут.", "voice": "Maxim" } } ], "else": [ { "tts": { "text": "Расчетное время ожидания до соединения с оператором меньше двух минут.", "voice": "Maxim" } } ] } } ] } } } ]
Довольно часто наши клиенты сталкиваются с необходимостью выгрузить информацию о звонках из раздела Call Detail Record к себе. Как вы уже знаете (даже если взять во внимание предыдущую запись в этом блоге, а именно 📲 Липкость звонка), вся историческая статистика хранится в базе elasticsearch. В документации вы так же могли увидеть пример получения данных с помощью нашего REST API. Но, что если звонков несколько сотен? Или несколько тысяч? Как правильно получить такой объем данных? Сегодня я расскажу как работать с большими объемами данных.
Scroll
Для получения большого количества данных, в elasticsearch предусмотрен функционал scroll, который мы повторили и в нашем REST API. Рассмотрим на примере:
{ "scroll" : "5m", "limit": 1000, "sort": { "created_time": { "order": "desc", "unmapped_type": "boolean" } }, "index": "cdr-a", "query": "*", "columns": [ "created_time", "uuid", "direction", "duration" ], "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/w", "lte": "now" } } } ] } } ] }
В тело нашего запроса мы добавили 2 новых параметра:
- scroll - как долго на сервере держать результат запроса
- limit - какими порциями возвращать результат запроса
Дальше, выполняем первый запрос с указанным телом на REST API, для простоты я использую консольную утилиту cURL:
curl -s -L -XPOST \ -H 'Content-Type: application/json' \ -H 'X-Access-Token: ciOiJIUzI1NiJ9.jEyM2UxNThjLWVkNzMtNDAwi'\ "https://pre.webitel.com/engine/api/v2/cdr/text" -d@webitel_scroll_request.json
Вместе с результатом, который не будет превышать заданного в limit значения, мы получим _scroll_id:
Теперь все последующие запросы мы выполняем уже с scrollId в теле запроса. Пример:
curl -s -L -XPOST \ -H 'Content-Type: application/json' \ -H 'X-Access-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6I9.IjKpitL05OLjUPeUQyd4E'\ "https://pre.webitel.com/engine/api/v2/cdr/text/scroll" -d ' { "scroll": "5m", "scrollId": 'МНОГО_БУКВ_ПОЛУЧЕННОГО_ИД' }'
Повторяем запрос, пока не заберем все данные с сервера.
Бонус
В качестве бонуса, подготовил небольшой bash скрипт, который с помощью cURL и jq поможет вам выкачать необходимые данные и сохранить в CSV файл:
Тело запроса должно находится в файле webitel_scroll_request.json в возле данного скрипта.
Удачи с запросами!
Использование IVR меню, в большинстве случаев, позволяет эффективно распределять входящие звонки и снижать нагрузку с секретаря. Клиент звонит, выбирает нужный отдел либо донабирает внутренний номер сотрудника и достигает цели. Но, как быть в ситуации, когда клиент не знает кому он звонит? Что если это наш менеджер не смог дозвонится клиенту, который перезвонил и попал на IVR? В данном случае будет полезным функционал "Липкости звонка". Мы уже рассматривали реализацию: Входящий звонок с маршрутизацией на ответственного в bpm'online. Сегодня мы рассмотрим пример маршрутизации в первую очередь на того, кто сегодня уже общался с данным номером либо звонил ему. Приступим!
Kibana нам в помощь
Для решения поставленной задачи нам понадобится приложение cdr, которое предназначено для поиска по журналу звонков в elasticsearch. В начале, нам нужно написать правильный запрос. Делаем тестовый звонок и открываем интерфейс Kibana, раздел Discover. Выбираем для отображения колонки extension (номер сотрудника), caller_id_number (номер абонента), destination (номер назначения).
Теперь сделаем запрос с фильтрацией по номеру абонента, он может быть либо в номере назначения (для исходящих) либо в номере абонента (для входящих):
destination_number:/0969716158/ OR caller_id_number:/0969716158/
Как результат, мы получаем номер сотрудника, extension, который общался либо звонил абоненту. И обязательно открываем Response (ответ со стороны elasticsearch), он нам пригодится ниже:
И так, как искать мы уже поняли, а теперь, добавим это все в маршрутизацию на стороне Webitel.
Запрос в CDR
Для работы с приложением cdr, нам нужно написать правильно запрос в elasticsearch
{ "limit": 1, "sort": { "created_time": { "order": "desc", "unmapped_type": "boolean" } }, "index": "cdr-a", "query": "*", "columns": [ "extension" ], "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/d", "lte": "now" } } }, { "query_string": { "query": "destination_number:/.*${caller_id_number}/ OR caller_id_number:/.*${caller_id_number}/", "analyze_wildcard": true, "default_field": "*" } } ] } } ] }
Разберем запрос:
Параметр | Описание |
---|---|
limit | Количество записей. 1 - так как нам нужно только последнего звонившего. |
sort | Сортировку делаем от новых к старым, опять же - только последний. |
index | В каком индексе осуществлять поиск: cdr-a |
columns | Какие поля возвращать - нам нужен номер сотрудника: extension |
query_string | Строка запроса в которой номер телефона заменен на канальную переменную caller_id_number |
range | Временной интервал: начиная с 00:00 текущего дня до теперь. |
А теперь посмотрим на ответ со стороны elasticsearh:
{ "took": 4, "timed_out": false, "_shards": { "total": 10, "successful": 10, "skipped": 0, "failed": 0 }, "hits": { "total": 5, "max_score": null, "hits": [ { "_index": "cdr-a-2019-.bpmonline.com", "_type": "cdr", "_id": "bb643481-310a-45cb-8c4e-95244c8e02b8", "_score": null, "fields": { "extension": [ "115" ] }, "sort": [ 1547032525216 ] } ] } }
Что бы добраться к нужному полю extension, мы должны пройти путь: hits => hits => fields => extension
Теперь давайте обновим нашу маршрутизацию в Public
Маршрутизация входящего звонка
У нас уже есть готовое IVR меню, так что мы добавим вначале проверку, и если результат не пустой, попытаемся соединить с нужным сотрудников (а с помощью userData добавим еще условие, что бы он был в статусе onhook - Готов). Если сотрудник не ответит в течение 15 секунд, тогда уже продолжим выполнять нашу типовую IVR схему:
["начало схемы"], { "cdr": { "exportVar": { "last_cid": "hits.hits.0.fields.extension.0" }, "elastic": { "limit": 1, "sort": { "created_time": { "order": "desc", "unmapped_type": "boolean" } }, "index": "cdr-a", "query": "*", "columns": [ "extension" ], "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/d", "lte": "now" } } }, { "query_string": { "query": "caller_id_number:/.*${caller_id_number}/ AND destination_number:${destination_number}", "analyze_wildcard": true, "default_field": "*" } } ] } } ] } } }, { "if": { "expression": "${last_cid}", "then": [ { "userData": { "name": "${last_cid}", "var": "account_state", "setVar": "acc_state" } }, { "log": "CID: ${caller_id_number}, Ext: ${last_cid} (${acc_state})" }, { "if": { "expression": "${last_cid} && ${acc_state} == 'onhook'", "then": [ { "bridge": { "endpoints": [ { "name": "${last_cid}", "type": "user", "parameters": [ "leg_timeout=15" ] } ] } } ] } } ] } }, ["продолжение схемы"]
Все прекрасно знают, что с помощью Календаря мы можем настроить рабочий график офиса и добавить проверку в маршрутизации на рабочий\не рабочий день. Но, бывают ситуации, когда этого не достаточно. Мы хотим настроить разделение на рабочие, не рабочие и праздничные дни. Поскольку календарь умеет возвращать только истина либо ложь в отношение рабочего графика, то раньше для решения данной задачи приходилось создавать 2 календаря - в одном проверяем на праздники, а во втором - на рабочие. С релизом 3.10 нам достаточно 1 календарь :allthethings:
Как это работает? Появился новый параметр extended, который отключен, по умолчанию, для сохранения совместимости со старыми схемами. Проверяем календарь включенным параметром:
{ "calendar": { "name": "my Business Calendar", "extended": true, "setVar": "isWorkDay" } }
Теперь переменная isWorkDay может принимать следующие значения:
- true - сейчас рабочий день
- false - не рабочий день
- holiday - праздничный день
- ahead - календарь еще не стартовал
- expire - календарь уже завершился
Надеюсь, что такое изменение поможет эффективней создавать ваши схемы маршрутизации.
Сегодня мы поговорим о SIP телефонах. А именно, об опыте использования SIP телефонов в локальной сети офиса, которые подключаются к SIP серверу через публичную сеть Интернет. Если вы используете SIP-телефоны вместе с нашим облачным сервисом, то данная заметка будет полезна и поможет избежать основных проблем при работе IP телефонии за NAT.
Что такое NAT?
Начнем с того, а что же такое этот NAT?
Не буду копировать из wiki умные вещи, попробую объяснить проще - NAT (Network Address Translation) — это механизм, который позволяет маршрутизатору (наш сервер, роутер, модем – все, что используем для выхода в Интернет) определять какие сервисы находятся за роутером и должны быть доступны из интернета, чтобы пользователи оттуда могли этими сервисами пользоваться. Так как, в большинстве случаев, у нас всего 1 внешний (белый, публичный – как кому больше нравится) IP адрес, а устройств в сети много, то мы используем локальные (серые) IP адреса. Они не доступны из Интернета, а NAT помогает нам опубликовать в мир какой-то порт из локальной сети.
Надеюсь, что здесь пока все понятно…
Проблема телефонии за NAT
Начнем с голоса. Вряд ли стоит объяснять, почему NAT является проблемой для голосового трафика. Если SIP протокол использует 1 статический порт на аппарате, то для голоса такой порт назначается динамически при каждом новом звонке. Как результат, каждый из нас знаком с ситуацией в IP-телефонией, когда голос ходит только в одном направление или вовсе отсутствует. Сразу следует заметить, что универсального «лекарства» здесь нет: все решения этой проблемы в большей или меньшей степени частные. Впрочем, проблема обхода NAT голосовым потоком – это лишь одна сторона медали. Вначале мы рассмотрим, какие препятствия создаёт NAT для сигнальных сообщений (в таких случаях звонок вообще не попадает на телефон за NAT) и какие расширения SIP были разработаны для их преодоления.
Обход NAT SIP-сигнализацией
При использовании UDP протокола, отправка ответа на SIP-запрос осуществляется на тот IP-адрес, с которого запрос был получен. Номер же порта для отправки извлекается из заголовка Via в SIP пакете. В случае использования NAT – это порт, на котором ожидает ответа наш IP телефон, но точно не тот порт, через который происходит NAT-трансляция и на котором NAT ожидает поступления ответа, чтобы его дальше направить уже на телефон. Мы получаем ситуацию, в которой звонок не может достичь SIP телефона:
Для решения этой проблемы SIP умеет отправлять ответ на порт, с которого запрос был получен, вместо порта, взятого из заголовка Via. При этом сам порт заносится в специальный параметр rport-заголовка Via. Это позволяет ответу найти соответствие в таблице NAT и достичь целевого узла. Этот метод называется симметричной маршрутизацией ответов.
Другая проблема заключается в том, что каждая запись в таблице трансляции NAT автоматически удаляется после определенного промежутка времени. Это актуально не только для установления нового соединения, но и во время SIP-диалогов новые сообщения также могут не поступать в течение длительного промежутка времени, чего достаточно для того, чтобы запись из таблицы была удалена. В таких случаях мы можем наблюдать ситуацию, что звонок разрывается точно на 30-40 секунде.
Для решения данной проблемы, SIP умеет периодически отправлять запросы re-INVITE, OPTIONS, INFO, NOTIFY либо другие. В итоге мы получаем вот такую картинку:
Обход NAT медиатрафиком
Решение проблемы обхода NAT медиатрафиком требует более сложных изменений, поскольку необходимо заменить IP-адрес и номер порта, анонсированные в SDP-сообщении, таковыми, что обеспечат доставку потоков нужному адресату за NAT. Есть несколько способов решения данной проблемы, но, так как данная заметка уже получается довольно большой, я остановлюсь только на прохождении SIP, так как именно с этим чаще всего сталкиваются наши клиенты.
Рекомендации по настройке SIP телефонов
Несколько основных рекомендаций по настройке SIP телефонов в сети (на примере аппарата Yealink).
RPort
Включить параметр rport:
Keep Alive
Включить и отправлять каждые 30 секунд Keep Alive. У других производителей телефонов может называтся: re-INVITE, OPTIONS, INFO, NOTIFY
Разные локальный SIP
В некоторых случаях помогает, если каждому аппарату задать свой уникальный локальный SIP порт:
TCP/TLS
Также можно использовать TCP либо TLS вместо UDP. В некоторых случаях это более надежное решение для обхода NAT. При использовании TLS, следует обратить внимание, что порт подключения к webitel нужно указать 5071, вместо стандартного для UDP/TCP 5070
Отдельная сеть
Хорошей практикой является выделение для SIP телефонов отельной локальной сети с QoS приоритезацией голосового трафика. Еще лучше – настроить отдельный VLAN под IP телефонию.
Поскольку все чаще встречаюсь с непониманием того, что творится у нас в настройках маршрутизации, решил сегодня написать несколько слов о регулярных выражениях
Кода Вы открываете настройку исходящей маршрутизации, то можете увидеть вот такой кошмар:
Давайте на данном примере попытаемся понять, что и как работает.
^\+?38?(0[679]3\d{7})$
Данной регулярное выражение описывает коды украинского мобильного оператора lifecell. У данного оператора есть 3 кода: 63, 73 и 93. Номера телефонов пользователи могут набрать как в международном формате +38063ххххххх, в национальном формате 073ххххххх, так и вообще в устаревшем формате: 8093ххххххх. Вот таким выражением мы закрываем все варианты набора номера.
Рассмотрим более детальней:
- ^ - начало регулярного выражения. Если не будет этого символа, тогда у нас получиться вхождение, а нам нужно нужно проверять с самого начала набранного номера. Значит для нас это обязательный символ.
- \+? - дальше проверка на наличие +, поскольку это служебный символ, то мы его экранируем с помощью \. А вот наличие знака вопроса, ?, означает не обязательность +. Он может быть, а может и не быть - как-то так...
- 3? - а это просто 3, которой вполне может и не быть (помните про наличие знака вопроса после цифры ?)
- 8? - здесь то все понятно?
- ( - начало блока совпадения. Все, что будет в круглый скобках, потом попадает в служебные переменные и может использоваться в маршрутизации звонков.
- 0 - обязательно должен быть 0. Посмотрите на примеры выше - 0 всегда присутствует.
- [679] - дальше должна идти одна из 3-х цифр: или 6, или 7 или же 9. Одна! Не три, а одна из 3 - просто уточняю
- 3 - а здесь обязательно должна быть тройка.
- \d - этот незамысловатый знак говорит нам о наличие любой цифры (то же самое, если бы я написал вот так: [0-9]).
- {7} - а теперь мы говорим о количестве повторений предыдущего выражения. Это означает, что любых цифр всего должно быть 7.
- ) - закрываем блок совпадения.
- $ - завершение регулярного выражения. Опять же, без него получается вхождение, поэтому для нас данный символ будет обязательным.
Для закрепления, рассмотрим еще один пример:
^\+?(7|8)(\d{10,12})$
Это регулярное выражение описывает все телефонные коды РФ (если честно, то и Казахстана, но, сейчас не об этом). Посмотрим только отличие от предыдущего
- 7|8 - вертикальная черточка говорит об ИЛИ. Может быть 8 либо 7 - одна из двух
- {10,12} - опять количество повторений предыдущего выражения. Но, здесь у нас диапазон - не меньше 10 и не больше 12.
Так же, в отличие от предыдущего выражения, здесь у нас дважды встречаются круглые скобки, а это означает, что мы отдельно можем работать с первым и вторым совпадением. Что это означает? Давайте посмотрим на очень полезную функцию, которая должна появиться в следующем релизе - тест регулярного выражения:
- ®0.$0 - если нам нужен номер целиком
- ®0.$1 - если только совпадение в первых круглых скобках
- ®0.$2 - если совпадение во вторых круглых скобках
Провайдер требует от нас всегда присылать номера в национальном формате через 8. Для того, что бы набранный мною номер +74997045627 уходил к провайдеру в формате 84997045627, наш bridge должен быть вот таким:
{ "bridge": { "endpoints": [ { "dialString": "8®0.$2", "name": "myMskGw", "type": "sipGateway" } ] } }
Надеюсь, что теперь стало немного понятьней, что такое регулярное выражение и как его правильно прочитать.
Приветствую!
Как известно в текущей версии дайлера webitel есть возможность загрузить абонентов из CSV файла:
Вы можете настроить сопоставление колонок в файле и выполнить импорт. Но, есть одно неудобство, если вам нужно каждый день выполнять такую загрузку, то каждый день вы сталкиваетесь с настройкой этих колонок. Уже довольно давно наши клиенты спрашивали, как это можно "запомнить", и мы решили в следующем обновление пересмотреть данный механизм. И так, у нас появляется новая вкладка Integrations, которая позволяет предварительно настроить нужные нам шаблоны для импорта абонентов либо выгрузки результатов работы дайлера:
Уже сейчас есть возможность работать с CSV файлами и с таблицами MSSQL (через внешнюю утилиту). Более того, для SQL предусмотрена настройка планировщика, который будет автоматически по расписанию забирать данные либо выгружать статистику.
Каких еще провайдеров были бы интересно увидеть в этом меню?
Пишите в комментариях :awthanks: