Как стать автором
Поиск
Написать публикацию
Обновить

Система мониторинга через jabber

Время на прочтение7 мин
Количество просмотров11K


Возможность мгновенного оповещения об определенных событиях требуется достаточно часто. Системные администраторы должны как можно быстрее узнавать о сбоях в работе сервисов и серверов, технический персонал на производстве — о сбоях и отклонениях в технологическом процессе, службы оперативного реагирования — о происшествиях. Самый очевидный способ оповещения — это оповещение по СМС. Для оповещения через СМС существуют специальные интернет сервисы, осуществляющие рассылку сообщений на заданную группу. Можно сэкономить и осуществлять рассылку самостоятельно, используя GSM модем. Но у этого способа есть несколько минусов: нужно уметь работать с последовательным портом, а через него с модемом, последовательно обрабатывать команды; отправлять сообщения по-русски не так просто; скорость отправки большому количеству адресатов может оказаться не достаточно быстрой; сложно обеспечить контроль доставки; нет гарантии, что сотовый оператор не заблокирует сим карту, если посчитает рассылки за спам. В целом, сервисы рассылки дают хотя бы какие-то гарантии, но стоят определенных денег.

Если же для отправки сообщения использовать сеть интернет, то можно сильно сэкономить на стоимости отправки. Сообщение для электронной почты легко сформировать и отправить. Но электронная почта не является оперативным каналом. Неизвестно, как скоро адресат прочитает сообщение, ведь не все почтовые клиенты оповещают о получении нового сообщения. Почтовый клиент на мобильном устройстве настроен далеко не у всех и не всегда.

Совсем другое дело — системы обмена мгновенными сообщениями (ICQ, XMPP). Протокол XMPP оказывается более предпочтительным благодаря тому, что он открыт. А благодаря тому, что это полноценный сетевой протокол, то получаются «из коробки» доступны следующие возможности:
  • список контактов может являться списком рассылки (этот список легко редактировать)
  • данные шифруются
  • контроль доставки
  • можно видеть статусы получателей (онлайн), чтобы понять, кто может получить сообщение
  • принимать сообщения можно как на персональный компьютер, так и на мобильное устройство и для этого не требуется разрабатывать специальную программу
  • оповещение можно расширить интерактивностью: добавить чат/конференцию, обработку дополнительных запросов

При желании список можно продолжить.

В качестве примера реализации данного подхода, разработана программа, которая оповещает об ошибках технологического оборудования. Стойка оборудования представляет из себя некую программу, которая пишет сообщения (и сообщения об ошибках в том числе) в базу данных. БД имеет формат — Paradox, а кодировка данных Win-1251. Было решено отказаться от графического интерфейса в пользу консольного приложения, параметры задавать текстовыми файлами. Инструмент для решения — QT.

Реализованный функционал: сбор ошибок с множества технологических установок, отправка сообщений через jabber, общий чат через jabber.

Архитектура и механизмы взаимодействия


Программа, которая ставится на конечное оборудование, условно названа «клиент». Программа, которая будет принимать данные от клиентов — «сервер». Обмен данных осуществлен через broadcast UDP. По принятым данным, сервер отправляет информацию по всему своему контакт листу jabber аккаунта.

Клиент


Параметры программы

Параметры решено хранить в json. Для работы используется 4-й QT, в нем еще нет работы с json, поэтому разбор производится с помощью сторонней библиотеки для QT (qt-json).

Файл с параметрами клиента database.json
{
"LogFileName":"c://manlog.log",
"ConnectionString":"Driver={Microsoft Paradox Driver (*.db )};DriverID=282;FIL=Paradox 4.x;DBQ=c:\\;ReadOnly=1",
"CNCName":"CNC_01",
"RefreshPeriod":"2000"
}


Чтение параметров:
    QFile configFile("./config/database.json");
    configFile.open(QIODevice::ReadOnly | QIODevice::Text);
    QTextStream dbFileIn(&configFile);
    QString jsonData = dbFileIn.readAll();
    configFile.close();

    bool ok = false;

    QVariantMap result = QtJson::parse(jsonData, ok).toMap();

    if(ok) {       
        foreach(QVariant key, result.keys()) {
            if(key.toString().toLower()=="connectionstring")
                connecionString = result.value(key.toString()).toString();
            if(key.toString().toLower()=="logfilename")
                logFileName = result.value(key.toString()).toString();
            if(key.toString().toLower()=="refreshperiod")
                refreshPeriod = result.value(key.toString()).toString().toInt();
            if(key.toString().toLower()=="cncname")
                cncName = result.value(key.toString()).toString();
        }
    }


Подключение к БД PARADOX

Для доступа к БД из QT есть QODBC. Строка соединения для доступа к PARADOX:
"Driver={Microsoft Paradox Driver (*.db )};DriverID=282;FIL=Paradox 4.x;DBQ=c:\\;ReadOnly=1"

где DBQ — путь к БД.
Важный момент: в строке подключения после "*.db" перед ")" стоит пробел!

Строку соединения решено было хранить в файле рядом с программой, так как настройка источника данных через системные источники — это долго и неудобно. Да и вообще хорошо, когда все настройки в одном месте.

Программа на стойке пишет лог в таблицу ManLog.
Имя поля Тип Назначение
Data Date Дата занесения записи в лог
Time DateTime Время занесения записи в лог
Mes Varchar Сообщение

В типах данных столбцов могу ошибаться. И QT в них тоже ошибается, поэтому при составлении SQL запроса пришлось хитрить: преобразовывать время в строку, потому что взять поле, как toDateTime() не удалось.

SELECT data, format(ManLog.Time,'hh:mm:ss'), mes from ManLog


На этом проблемы с подключением к БД не закончились. Как показал просмотр файла БД, данные там в кодировке win-1251. У меня уже был опыт работы с кодеками, преобразованием кодировок, настройке кодировок в BDE, настройках реестра. Все это может по-разному работать на разных ОС. Какая ОС будет стоять на стойке неизвестно, и не стоит лишний раз вмешиваться в работу ОС промышленного оборудования. Поэтому было решено брать данные из поля Mes в двоичном виде:
    QSqlQuery query;
    query.exec("SELECT data, format(ManLog.Time,'hh:mm:ss'), mes from ManLog");
  ...
    QByteArray msg = query.value(2).toByteArray(); //двоичные данные!

Данные считаны из БД, а теперь их надо сверить с кодами ошибок. Но коды ошибок нам нужны тоже в двоичном виде в той кодировке, в которой они в базе. В json коды ошибок прописать можно, но при чтении json файла мы опять сталкиваемся с проблемой кодировки. Поэтому, нет времени объяснять, создаем ini файл и вбиваем туда ошибки windows блокнотом:

Файл со списком ошибок

errorlist.ini — файл со списком ошибок
Превышена предельно допустимая температуры чего-нибудь
и т.д.

Считываю данные в список бинарных массивов:
    QFile fileIni("./config/errorlist.ini");
    fileIni.open(QFile::ReadOnly);
    QByteArray errorRaw = fileIni.readAll();
    fileIni.close();
    errorRaw = errorRaw.replace('\n', "");
    QList<QByteArray> errorAllList = errorRaw.split('\r');
    for(int i=0;i<errorAllList.count();i++) {
        if(errorAllList.at(i).count()>0) {
            errorList << errorAllList.at(i);
        }
    }


Дальше мы заводим список уже считанных строк, чтобы не посылать их повторно по сети. Запускаем таймер с периодом refreshPeriod и делаем перезапрос данных. Если есть новые данные — отправляем их по сети через UDP.

Отправка данных по сети

Данные решено сериализовать с помощью QDataStream. Отправляется имя станка, дата, время, признак — является ли строка ошибкой, и само сообщение в двоичном виде.

Определяем есть ли ошибка в строке
        QString errText="ok";
        foreach(QByteArray err, errorList) {
            if(msg.contains(err)) {
                errText = "ERROR";
            }
        }


Сериализуем, отправляем:
        QByteArray datagram;
        QDataStream out( &datagram, QIODevice::ReadWrite );
        out.setVersion(QDataStream::Qt_4_0);
        out << cncName;
        out << query->value(0).toDate(); //date
        out << logTime;//time
        out << errText;
        out << msg;//bytearray

        udpSocket->writeDatagram(datagram, QHostAddress::Broadcast, 45000);


Серверная часть



Состоит из двух частей: прием UDP сообщений и отправка их по xmpp.

Принимаем UDP пакеты

Слушаем порт:
    udpSocket = new QUdpSocket(this);
    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));

    udpSocket->bind(45000);

Разбираем пришедшее сообщение и испускаем сигнал, если у записи есть признак «ошибка».
void CncReceiver::onReadyRead()
{
    QByteArray buffer;
...
    buffer.resize(udpSocket->pendingDatagramSize());

    udpSocket->readDatagram(buffer.data(), buffer.size());

    QDataStream in( &buffer, QIODevice::ReadWrite );
    in.setVersion(QDataStream::Qt_4_0);

    in >> cncName;
    in >> date;
    in >> time;
    in >> errText;
    in >> msg;
...
    if(errText!="ok")
        emit needSendToAll(msg, cncName, date, time);

}

Этот сигнал будет принимать класс, умеющий отправлять сообщения наружу. В нашем случае в Jabber. По аналогии можно отправлять на почту или по смс.

Отправляем сообщения в jabber

Для отправки сообщений создаем класс наследник от QXmppClient из этой замечательной библиотеки(qxmpp).

Для хранения списка рассылки используем
QList<QString> sendList;

Наполним его данными из ростера jabber аккаунта (там находятся имена аккаунтов).

А слот, который ловит сигнал needSendToAll, отправляет сообщение всем из sendList.
void JabberClient::sendToAll(QByteArray msg, QString cncName, QDate errDate, QString errTime)
{
    QString messageToUser;

    QTextCodec *codec = QTextCodec::codecForName("Windows-1251");

    messageToUser = codec->toUnicode(msg);

    foreach(QString userName, sendList) {
        this->sendMessage(userName, "CNC_NAME: "+ cncName +  "\n" +
                                    errDate.toString("yyyy-MM-dd ")+ errTime + "\nMesage:\n" + messageToUser);
    }
}


Реализация чата в jabber

Все сообщения, полученные аккаунтом отправляются на весь список контактов с припиской имени отправителя. Таким образом, получается общий чат. Может быть удобен для обсуждений.

Добавляется очень просто. Для этого в конструкторе JabberClient добавляем обработку еще одного сигнала — сигнала приема сообщения:
    connect(this, SIGNAL(messageReceived(QXmppMessage)), SLOT(onMessageReceived(QXmppMessage)));

И обрабатываем его в слоте приема сообщения:
void JabberClient::onMessageReceived(QXmppMessage msg)
{
    if(msg.body().length()>0) {
        qDebug() << nowStr() << "onMessageReceived" << msg.from() << msg.body();
        foreach(QString userName, sendList) {
            this->sendMessage(userName, msg.from() + ">\n" + msg.body());
        }
    }
}

Проверка длины сообщения из за того, что в процессе набора сообщения срабатывает вызов, но длина сообщения равно 0.

Итоги


Проект является примером того, как можно подключаться из QT к базам данных с заданной кодировкой сообщений, не привязываясь и не настраивая локаль системы. Так же показано, как применить библиотеку qxmpp в QT для отправки сообщений в jabber.

Исходные коды примеров:
code.google.com/p/cnc-error-monitor

PS. Статья будет дополняться и обновляться
UPD: исправлен разбор параметров из json, реализован чат через jabber
UPD2: странное замечание: если собрать клиентскую часть на QT 4-й версии, то данные из byteArray из БД читаются так, как нужно — в кодировке базы. А если собрать на QT5, то данные там получаются испорченными (и никакие параметры не помогают).
Теги:
Хабы:
Всего голосов 19: ↑16 и ↓3+13
Комментарии20

Публикации

Ближайшие события