Pull to refresh

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

Reading time7 min
Views11K


Возможность мгновенного оповещения об определенных событиях требуется достаточно часто. Системные администраторы должны как можно быстрее узнавать о сбоях в работе сервисов и серверов, технический персонал на производстве — о сбоях и отклонениях в технологическом процессе, службы оперативного реагирования — о происшествиях. Самый очевидный способ оповещения — это оповещение по СМС. Для оповещения через СМС существуют специальные интернет сервисы, осуществляющие рассылку сообщений на заданную группу. Можно сэкономить и осуществлять рассылку самостоятельно, используя 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, то данные там получаются испорченными (и никакие параметры не помогают).
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+13
Comments20

Articles