Привет, Хабр! Состояние сервера необходимо постоянно мониторить, чтобы в случае ошибки быстро ее исправить. Удобнее всего отслеживать работоспособность с помощью смартфона, а именно Telegram-бота, он позволяет осуществлять проверку на ходу. Реализуем Telegram-бот на функциональном языке Haskell, заодно разберемся в его преимуществах.  

Одним из преимуществ функциональных языков является удобство обработки списков из-за встроенного механизма рекурсий и сравнения образцов, но многие разработчики не используют их для разработки реальных приложений, в которых такие задачи встречаются. Рассмотрим возможность применения функционального языка Haskell на задаче мониторинга состояния серверов и анализа их логов. Задача является важной, так как чем дольше сервер находится в неработоспособном состоянии, тем больше проблем это приносит его пользователям.

Основная задача программы – регулярно осуществлять проверку серверов пользователя, записывать результаты в базу данных и при необходимости, выдавать собранную статистику.

Для функционирования программы необходимо создать базу данных и три таблицы:

  1. Users – таблица с информацией о пользователях;

  2. UrlList – хранит URL-адрес, состояние сервера на момент проверки и ID пользователя, добавившего адрес;

  3. UrlState – в данной таблице сохраняются логи обращений к серверу, а именно: имя сервера, состояние и дата состояния.

Схема таблиц

В данной программе взаимодействие с базой данных производится с помощью модуля Database.SQLite.Simple. Рассмотрим пример функции, которая добавляет новый URL-адрес. Необходимо открыть соединение с базой данных и передать функции SQ.execute открытое соединение и текст запроса на языке SQL, затем закрыть соединение.

Листинг 1. Пример вставки новой строки в таблицу.

insertNewUrl :: String -> Int -> IO ()
insertNewUrl urlstr usrId = do
  db_conn <- SQ.open "test2.db"
  SQ.execute db_conn "INSERT INTO UrlList (site_url, state, userId) VALUES (?, 1, ?)"   ( urlstr :: String, usrId :: Int)
  SQ.close db_conn

Все взаимодействие с пользователем осуществляется через Telegram-бот. У него есть несколько основных команд:

  1. Add url – добавляет новый URL-адрес;

  2. Remove url – удаляет данный URL-адрес;

  3. Show – показывает список URL-адресов текущего пользователя;

  4. Analyze url – производит анализ логов данного URL-адреса;

  5. Ping url – отправляет запрос на данный URL-адрес, ждет ответ и выводит актуальную статистику.

Для взаимодействия с Telegram-ботом используется функция handleAction, которая обрабатывает все активности в чате. Функция определяет пользователя: если текущего пользователя нет в базе данных, то в чат отправляется сообщение со списком возможных команд и новый пользователь записывается в таблицу Users. Если пользователь известен, то команды из чата анализируются и обрабатываются с помощью конструкции case. Для каждой команды предусмотрены свои функции и ответы в чате.

Листинг 2. Пример взаимодействия с Telegram-ботом.

handleAction :: Action -> ChatModel -> Eff Action ChatModel
handleAction action model =
  case action of
    NoAction -> pure model
    RecordMsg usrId usrname chtId msgId txt ->
      model <# do
        let cId = fromIntegral chtId ::Int
        maybeUser <- liftIO $ getUserById usrId
        case maybeUser of
          Just user -> do
            now <- liftIO getCurrentTime
            comand <- liftIO $ getCommand txt 
            liftIO $ print comand
            url <- liftIO $ getUrl txt
            liftIO $ print cId
            let chtId2 = ChatId chtId
            let chatId=SomeChatId chtId2
            case comand of
              "add" -> do
                liftIO $ insertNewUrl url usrId
                replyString "url added"
              "remove" -> do
                liftIO $ delSrv url
                replyString "url removed"
              "show" -> do
                sitelist<-liftIO $ getUrlList usrId
                replyString (getUrlListTxt sitelist)
              "help" -> do
                replyString (unpack  startMessage)
              "analyze" -> do
                stateList <- liftIO $ getUrlState url
                replyString (analyseLog stateList 1 0 url)
              "ping" -> do
                liftIO $ print ("pinging"++url)
                liftIO $ pingServer url
                sitelist<-liftIO $ getUrlList usrId
                replyString (getUrlListTxt sitelist)
              "pingAll" -> do
                  liftIO $ doTesting
                  sitelist<-liftIO $ getUrlList usrId
                  replyString (getUrlListTxt sitelist)
              _ -> do
                replyString (unpack  startMessage)
              
          Nothing -> do
            userKy <- liftIO $ (createUser usrId usrname cId)
            replyString "Hi. There's a command list: \n 1) add url \n 2) remove url \n 3) show - print all urls \n 4) ping - to ping all added url"

Рассмотрим подробнее основные функции. Функция ping принимает на вход URL-адрес и пытается получить ответ от сервера. Для работы с http-запросами в Haskell используется модуль Network.HTTP.Simple. С помощью функции httpLBS получаем ответ от сервера: если произошла ошибка и к серверу невозможно подключиться, то фиксируем состояние ошибки в базе данных в таблице с логами. Если ошибки не произошло, то фиксируем нормальное состояние.

Листинг 3. Пример работы с HTTP-запросами.


"ping" -> do
                liftIO $ print ("pinging"++url)
                liftIO $ pingServer url
                sitelist<-liftIO $ getUrlList usrId
                replyString (getUrlListTxt sitelist)
pingServer :: String -> IO()
pingServer url = do 
    req <- parseRequest url
    response2 <- try $ httpLBS req 
    case response2 of
        Left e -> saveErrorState url e
        Right result -> saveOkState url

Функция analyze изначально вызывает функцию getUrlState для получения логов состояний по данному URL-адресу, при этом логи возвращаются отсортированными от более новых записей к более старым. Затем полученная информация передается в функцию analyseLog, которая принимает следующие параметры: массив логов, состояние, количество попыток соединения и имя сервера. Рекурсия, заложенная в функциональных языках, позволяет удобно обрабатывать логи, хранящиеся в массиве.

Листинг 4. Пример рекурсивной обработки списка.

"analyze" -> do
                stateList <- liftIO $ getUrlState url
                replyString (analyseLog stateList 1 0 url)

getUrlState :: String -> IO ([UrlState])
getUrlState url= do
  db_conn <- SQ.open "test2.db"
  res <- SQ.queryNamed db_conn "SELECT site_url, url_state,statetime FROM UrlState WHERE site_url = :site_url ORDER BY rowid DESC" [":site_url" := url] :: IO [UrlState]
  SQ.close db_conn
  return res

analyseLog :: [UrlState] -> Int -> Int -> String -> String
analyseLog ((UrlState url 1 wtime):t) 1 cnt site = site++", all is ok. Pinging started at "++show wtime
analyseLog ((UrlState url 0 _):t) 1 cnt site = analyseLog t 0 (cnt+1) site
analyseLog ((UrlState url 0 _):t) 0 cnt site = analyseLog t 0 (cnt+1) site
analyseLog [] 1 cnt site = site ++ " - missing. You can add him. Comand: add "++site
analyseLog [] 0 cnt site = site ++ " - never working. The call count is "++show cnt
analyseLog ((UrlState url 1 time1):t) 0 cnt site=  "server " ++ url ++ " is not working. The last working time is "++ formatTime defaultTimeLocale "%Y/%m/%d %H:%M" time1

Основная задача функции analyseLog – сформировать статистику работоспособности сервера. Если состояние сервера на момент последней проверки в логе равно «1», то функция возвращает сообщение о том, что сервер функционирует нормально, а также время последней проверки.

Если состояние сервера на момент последней проверки – «0», но до этого в логе видно, что сервер работал, то выдается сообщение о том, что сервер находится в неработоспособном состоянии, а также его последнее время работы.

Если в логах всегда встречается состояние «0», то выводится сообщение о том, что сервер никогда не работал, а также количество попыток связаться с ним.

В приведенном примере мы можем увидеть реализацию полноценного приложения на функциональном языке. В приложении сочетаются методы взаимодействия с HTTP серверами, Telegram-ботом, базой данных и применение рекурсий для анализа логов. Для чистых функций Haskell хранит результаты вычисления в кэше, поэтому, если функция вызывается с теми же параметрами, она не будет выполняться, а возьмет результат из кэша. Эта особенность позволяет сэкономить время на выполнение некоторых функций. Полный код программы вы можете найти на моем GitHub: ссылка на GitHub.

Поделитесь своим опытом разработки на функциональных языках в комментариях!

Комментарии (0)