Голосовое оповещение для Nagios

Типичное использование Nagios предполагает использование одной или нескольких панелей, на которых отображаются все происходящие события и открыты вкладки с наиболее важными показателями. И типичная хотелка к такой системе - а вот бы еще мониторинг все сообщения проговаривал вслух! Ну что, тогда такой задачей и займемся.
Готового решения мне найти не удалось, так что придется велосипедить.

Общая схема следующая:

1. В Nagios происходит событие.
2. Обработка события передается на скрипт  libexec/audio-host-notify.sh или libexec/audio-service-notify.sh.
3. Указанные обработчики частично переводят текст события на русский язык и отправляют  на веб-сервер.
4. Веб-сервер отправляет текст события в браузер пользователя системы мониторинга.
Таким образом мы получили текст события в браузер, но нам надо его озвучить.
5. Браузер вставляет HTML5 тег Audio, в котором в качестве источника звука указывается специальный сервер озвучивания текст. Текст закодирован в URL типа <audio src="http://text_to_speach/?text=hello_world"  />
6. Сервер озвучивания текста, получает текст и, при помощи речевого синтезатора, создает wav-файл и отдает его в браузер.
7. Браузер воспроизводит звук.


При этом, практическая схема немного сложнее, потому как текст, попадающий в браузер, должен вставть в очередь, иначе, Nagios при очередном обходе может нагенерировать десятки событий и они разом начнут воспроизводиться в десятки голосов.
Еще одним моментом является выбор: на чем писать веб-сервер для шагов 3-4 и на чем писать сервер для шага 6. Мне интересно было попробовать новую для себя технологию nodejs, и именно этот сервер я и использовал.
Преймущество схемы в том, что на шагах 5 и 6 будет создан веб-сервис, который можно использовать в самых разнообразных проектах, требующих преобразование текста в голос, причем преобразование будет осуществляться простым http-запросом.

Пути я буду писать для Nagios 3, собранного из исходников в CentOS6 и размещенного в /usr/local/nagios

Шаг 1. В Nagios происходит событие

В /usr/local/nagios/etc/objects/commands.cfg опишем команды для воспроизведения сообщений при проблеме с хостом и сервисом как:
 define command{
        command_name    host-notify-by-audio
        command_line    /usr/local/nagios/libexec/audio-host-notify.sh '$NOTIFICATIONTYPE$' '$HOSTALIAS$' '$HOSTSTATE$' '$HOSTOUTPUT$'
}
define command{
        command_name    service-notify-by-audio
        command_line    /usr/local/nagios/libexec/audio-service-notify.sh '$NOTIFICATIONTYPE$' '$SERVICEDESC$' '$HOSTALIAS$' '$HOSTADDRESS$' '$SERVICESTATE$' '$LONGDATETIME$' '$SERVICEOUTPUT$'
}
Эти две команды запускают скрипты audio-host-notify.sh и audio-service-notify.sh для шага 2 и их надо прописать контакту, который получает все озвучиваемые события. Скорее всего это будет тотже самый контакт, который показывает все события в центре мониторинга. Итак, этому контакту в его профиль (в /usr/local/nagios/etc/objects/contacts.cfg) прописываем:
host_notification_commands      host-notify-by-audio
service_notification_commands   service-notify-by-audio
В итоге, контакт может выглядить так:
define contact{
        contact_name                    panel
        use                             generic-contact
        alias                           Panel of Monitoring Center
        email                           noc@example.com
        host_notification_commands      host-notify-by-audio
        service_notification_commands   service-notify-by-audio
        }

Шаг 2. Обработка события передается на скрипт  libexec/audio-host-notify.sh или libexec/audio-service-notify.sh

Теперь создадим скрипты audio-service-notify.sh и audio-host-notify.sh.

Содержимое audio-host-notify.sh

#!/usr/bin/perl
do '/usr/local/nagios/libexec/translate.pl';
#my $string = '/usr/bin/curl -G --data-urlencode "text='.&string_translate($ARGV[0]).' Хост: '.&string_translate($ARGV[1]).' '.&string_translate($ARGV[2]).'" http://localhost:81/send/msg';
my $string = '/usr/bin/curl -G --data-urlencode "text=Насяльника! '.&string_translate($ARGV[0]).' Твоя: '.&string_translate($ARGV[1]).' '.&string_translate($ARGV[2]).' аднака" http://localhost:81/send/msg';
my $res = exec($string);
Содержимое audio-service-notify.sh:
#!/usr/bin/perl
do '/usr/local/nagios/libexec/translate.pl';
my $string = '/usr/bin/curl -G --data-urlencode "text=Насяльника! '.&string_translate($ARGV[0]).' Сервиса: '.&string_translate($ARGV[1]).' на машина '.&string_translate($ARGV[2]).' совсем-совсем '.&string_translate($ARGV[4]).' аднака." http://localhost:81/send/msg';
my $res = exec($string);
Здесь в /usr/local/nagios/libexec/translate.pl находится функция string_translate, заменяющая английские слова на русские. В итоге, будет сформирован запрос типа http://localhost:81/send/msg?text=Насялника! Внимание. Сервиса: бла-бла-бла на машина 1.1.1.1 совсем-совсем плоха аднака. и при помощи curl сообщение будет отправлено на сервер nodejs, висящий на 81 порту системы мониторинга.

Шаг 3. Указанные обработчики частично переводят текст события на русский язык и отправляют текст события на веб-сервер.

 В /usr/local/nagios/libexec/translate.pl должна быть определена функция string_translate и сам массив слов какие на что будем переводить. Как уже стало понятно, я перевожу на русский язык в исполнении Джамшута. Так получается веселее и, в итоге, постоянно звучащие сообщения не вызывают раздражения.
Содержимое /usr/local/nagios/libexec/translate.pl:
#!/usr/bin/perl

sub string_translate {
  my $string;
  foreach  $param (@_) {
    @words = split(" ",$param); # Разобьем строку на отдельные слова и будем переводить каждое слово в отдельности.
    foreach $en (@words) {
      $string .= &word_array($en)." ";
    }
  }

  return $string;
}

sub word_array() {
  $en = @_[0];
  %enru = (
        "UP",           "Сделалось",            # восстановлен
        "DOWN",         "Упала",                        # упал
        "Host",         "Хост",
        "HOST",         "ХОСТ",
        "Service",      "Сервис",
        "SERVICE",      "СЕРВИС",
        "CRITICAL",     "Плохая",                       # критичном
        "OK",           "Холосая",                      #нормальном
        "PROBLEM",      "Сламалося",                            # СБОЙ
        "RECOVERY",     "ПОЧИНИЛОСЯ",           # ВОССТАНОВЛЕНИЕ
        "WARNING",      "СТРАШНО",
        );
  if (exists($enru{$en})) { return $enru{$en}; }                # Возвращаем русское слово если оно есть в массиве
  return $en;                                                   # Если слово перевести не удалось, возвращаем все как есть.
}
Смысл происходящего описан в комментариях и, если надо добавить перевод или изменить текущий, достаточно в translate.pl в хеш-массиве %enru добавить соответствующие слова с переводами.

Шаг 4. Веб-сервер отправляет текст события в браузер системы мониторинга.

Итак, на localhost:81 отправляется сообщение. На этом порту сидит демон, написанный на nodejs и все что он делает, это отправляет текстовые сообщения в браузеры подключенных клиентов, желающих воспроизводить звуковые сообщения. Существуют разные способы отправить сообщение со стороны сервера в браузер. Я выбрал технологию веб-сокетов. 
Итак, первым делом надо будет установить nodejs с двумя модулями express и socket.io. Установка nodejs и модулей описана в документации и в большом количестве статей в интернете. Сам сервер будет состоять всего из трех файлов. Два статичных файла index.html и jquery.js, необходимые для браузера, и один - серверный скрипт index.js, который будет запускаться при помощи nodejs и выполнять все операции.
Создадим папку /opt/monitoringpanel и проинсталлируем в нее при помощи npm модули express и socket.io:
# mkdir /opt/monitoringpanel
# cd /opt/monitoringpanel
# npm install express
# npm install socket.io
 создадим в папке minitoringpanel скрипт index.js со следующим содержимым:
var app = require('express').createServer()
  , io = require('socket.io').listen(app);

var sock = new Object;

app.listen(81);  

app.get('/', function (req, res) {
  res.sendfile(__dirname + '/index.html');
});

app.get('/jquery.js', function (req, res) {
  res.sendfile(__dirname + '/jquery.js');
});

app.get('/send/msg', function (req, res) {
 // res.writeHead(200,{'Content-Type': 'text/plain'});
  console.log(req.param('text','none message'));
  if ( getClientAddress(req).toString() == '127.0.0.1') {
    res.end();
    console.log('message emited');
    io.sockets.emit(req.param('group','monitoringpanel'),{ msg: req.param('text','none message') });
  }
  else {
    res.send('permission denied');
    res.end();
  }
});

io.sockets.on('connection', function (socket) {
//  socket.emit('monitoringpanel', { msg: 'Моя пошел работать.' });
  socket.on('my other event', function (data) {
    console.log(data);
  });
});

// get client ip address
function getClientAddress(req) {
  return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
}
Скачайте в эту папку последнюю версию jquery с http://jquery.com/ и разместите ее версию как /opt/monitoringpanel/jquery.js

Создайте файл index.html со следующим содержимым:

<html>
<head>
<script src="http://monitoring.example.com:81/jquery.js"></script>
<script>
var msg_queue = [];
msg_queue[0] = 'queue is started';
//msg_queue[1] = 'Здравствуй, хозяина.';
// msg_queue[2] = 'Я пришел работать.';
var active_msg = 0;
var playing = false;

function queue_add_msg(msg) {
  msg_queue[msg_queue.length] = msg;
}

function queue_number() {
  jQuery('#queue-number').html(msg_queue.length-active_msg);
}

function play_next() {
  if ( active_msg < (msg_queue.length-1) ) {    // if active_msg is not last, then play active_msq+1
    playing = true;
    active_msg++;
    queue_number();
    jQuery('#msg-block').prepend("<div id='msg-block-"+active_msg+"' style='border: 1px; border-bottom: 1px solid #ccc;'>"+msg_queue[active_msg]+"</div>");
    jQuery('#wav-block').html('<audio controls="controls" autoplay onended = "play_next();" > <source src="http://text_to_speech.example.com/wav?text=' + msg_queue[active_msg] +'" type="audio/wav" mediagroup="monitoring" />
<source src="http://text_to_speech.example.com/mp3?text=' + msg_queue[active_msg] +'" type="audio/mpeg" mediagroup="monitoring" />You browser not support audio!</audio>');
  }
  else if ( active_msg == msg_queue.length-1 ) {                // if not more message
    playing = false;
  }
  else {alert(active_msg); alert( msg_queue.length);}
}
</script>
<script src="http://monitoring.example.com:81/socket.io/socket.io.js"></script>
<script>
  var socket = io.connect('http://monitoring.example.com:81');
  socket.on('monitoringpanel', function (data) {
    queue_add_msg(data['msg']);
    if (!playing) { play_next(); }
  });
</script>
</head>
<body onLoad='play_next();'>
<div id="queue-number"></div>
<div id="wav-block" style="height:70px;"> start <br />
</div>
<div id="msg-block"></div>
</body>
</html>
В этом файле замените выделенные жирным monitoring.example.com на адрес сервера мониторинга, на котором все это запускается, а text_to_speech.example.com на адрес сервера, занимающегося преобразованием текста в звук.

После того, как все написано, можно запустить сервер nodejs:
# cd /opt/monitoringpanel/
# /usr/local/bin/node index.js

Шаг 5. Браузер вставляет HTML5 тег Audio.


Если ошибок запуска nodejs нет, то можно открыть в браузере сервер мониторинга http://monitoring.example.com:81/index.html
Для того, чтобы проверить что сообщения в браузер приходят, можно написать простой скрипт /srv/scripts/message.sh:
#!/bin/bash
/usr/bin/curl -G --data-urlencode "text=$1" http://localhost:81/send/msg
 И вызывать его с указанием текста сообщения:
/srv/scripts/message.sh hello
И, если все работает, в браузер должно прийти сообщение hello:

Мы должны увидеть html5-плеер, но звука мы все еще не услышим, так как сервер text_to_speech.example.com еще не работает. Ну, значит, пора заняться им тоже.

Шаг 6. Сервер озвучивания текста, получает текст и, при помощи речевого синтезатора, создает wav-файл и отдает его в браузер.

 Нам надо написать веб-сервер, который на хводе получает url типа http://text_to_speech.example.com/wav?text=текст%20сообщения, преобразует текст в звук и в виде wav-файла отдает обратно. Почему это нельзя было сделать сразу на сервере мониторинга? Доступный синтезатор текста в звук под Linux мне известен только festival, а он плохо воспроизводит русскую речь. Поэтому, решено использовать синтезатор голоса под Windows, для которого есть хорошие голоса.
Под Windows возможны тоже разные варианты как и чем сделать, я сделал с использованием программы Govorilka CP (она бесплатна), а русский голос надо под нее купить.
Из голосов мы выбрали голос Николая.
Приводить и как-то разбирать код веб-сервера на nodejs я не буду, поскольку он получился немного большим, лучше выложу его на github.

Установка папки с веб-сервером следующая:
1) Папку govorilka надо скачать с github и распаковать на сервере text_to_speech.example.com в корень диска c:\
2) Купить русский голосовой движок, совместимый с программой govorilka и установить его в качестве движка по умолчанию. В XP это "Панель управления" -> "Речь" -> "Выбор голоса".
3) Установить nodejs под Windows и запустить c:\govorilka\run_nodejs.bat
4) Скачать бинарную статическую сборку ffmpeg и положить ffmpeg.exe в c:\govorilka\bin\ffmpeg.exe

Попробую разъяснить последовательность программы на nodejs:
а) Получается текст сообщения
б) Считается md5-хеш от текста сообщения и, если такой текст уже раньше воспроизводился, то сразу отдается закешированный звуковой файл
в) Если текст сообщения раньше не воспроизводился, то текст пишется в папку c:\govorilka\tmp как md5("текст сообщения").txt.
г) запускается программа говорилка, в которую отдается файл с текстом (прогнанный через iconv для конвертации текста из utf-8 в cp1251.
д) говорилка генерирует wav-файл и складывает его с c:\govorilka\wav\md5("текст сообщения").wav.
e) ffmpeg конвертирует wav-файл в mp3 и складывает его в c:\govorilka\mp3\md5("текст сообщения").mp3
ж) Полученный файл отправляется по http в браузер в ответ на начальный запрос.

Шаг 7. Браузер воспроизводит звук.

Все, если все сделано правильно, можно повторить шаг 5 и, на этот раз, услышать звук. Если звук все равно не слышно, то попобуйте использовать в качестве браузера Mozilla Firefox или Google Chrome (опера глючит с веб-сокетами nodejs-библиотеки socket.io).

Дополнительные настройки

После того, как вы все сделали и убедились что все работает, необходимо заняться безопасностью системы. Дело в том, что здесь нигде логины и пароли не проверяются и нет никакой защиты на атаку сервиса или неавторизованное прослушивание сообщений. Так что необходимо закрыть порт 80 сервера text_to_speech.example.com и порт 81 сервера monitoring.example.com от всех недоверенных компьютеров.

Известные ошибки

1) Иногда браузер перестает воспроизводить звуки либо перестает получать сообщения от monitoring.example.com. Лечится перезагрузкой страницы. Требуется дополнительно заняться улучшением обработок ошибок в браузере.
2) Иногда сервер text_to_speech.example.com перестает записывать звуки. Лечится очисткой кешированных ответов (удаление всех файлов из c:\govorilka\tmp и c:\govorilka\wav).
3) В скрипт nodejs на text_to_speech.example.com не встроено защиты от конкурентного доступа. В результате, если сайт http://monitoring.example.com:81 будет открыт в двух браузерах, то оба они почти одновременно обратятся за генерацией звукового файла и может произойти удвоение сообщения. Решение: открывать сайт http://monitoring.example.com:81 только одним браузером. По хорошему, надо переписать код сервера.
4) Не удалось найти способа ускоренной записи сообщения в файл. В результате, при генерации звукового сообщения, время его генерации не меньше времени проигрывания звукового файла. В документации к говорилке написано, что эта возможность зависит от голосового движка.

Голосовое оповещение на мобильном устройстве

 Если у кого-то возникла идея написать простое приложение, состоящее из браузера с открытым monitoring.example.com:81, то делать этогшо не стоит. iOS не воспроизводит звук, а Android через некоторое время неактивности отключает звук даже при работающем Wi-Fi и активном соединении веб-сокетов.

Замечания

У меня сервер monitoring.example.com:81 находится на сервере мониторинга. Но, при желании, совершенно спокойно можно его вынести на отдельных сервер и заменить в audio-service-notify.sh, audio-host-notify.sh и в message.sh все http://localhost:81 на нужный сервер, а в index.js заменить авторизацию источника сообщения по ip-адресу
if ( getClientAddress(req).toString() == '127.0.0.1') {
на что-то другое.

При желании, можно вывести сообщения говорилки на основной экран nagios. Для этого можно вставить iframe на http://monitoring.example.com:81 в файл /usr/local/nagios/share/main.php или в него-же вставить код из файла http://monitoring.example.com:81

Заключение

Описанная система нуждается в доделке, на что у меня сейчас времени и желания нет. Да и сама схема получилась недостаточно надежной. Чтобы как-то ее стабилизировать, надо отказаться от веб-сокетов и, лучше от самой идеи генерации звука через браузер - как ни крути, а браузеры в режиме 24х7 для этой задачи недостаточно надежны. Еще один todo связан с возможностью работы на планшетах и телефонах: звук должен отдаваться в mp3, либо должна быть предусмотрена возможность вывода звука и в mp3 и в wav. Теперь при помощи ffmpeg звук конвертируется  в mp3.

Популярные сообщения из этого блога

Мастерим компьютер для прямых интернет трансляций и записи с видеокамеры или системы ВКС

Как оживить корпоративный сайт?

Обзор почтового клиента Pronto Pro!