Привет, Хабр! Состояние сервера необходимо постоянно мониторить, чтобы в случае ошибки быстро ее исправить. Удобнее всего отслеживать работоспособность с помощью смартфона, а именно Telegram-бота, он позволяет осуществлять проверку на ходу. Реализуем Telegram-бот на функциональном языке Haskell, заодно разберемся в его преимуществах.
Одним из преимуществ функциональных языков является удобство обработки списков из-за встроенного механизма рекурсий и сравнения образцов, но многие разработчики не используют их для разработки реальных приложений, в которых такие задачи встречаются. Рассмотрим возможность применения функционального языка Haskell на задаче мониторинга состояния серверов и анализа их логов. Задача является важной, так как чем дольше сервер находится в неработоспособном состоянии, тем больше проблем это приносит его пользователям.
Основная задача программы – регулярно осуществлять проверку серверов пользователя, записывать результаты в базу данных и при необходимости, выдавать собранную статистику.
Для функционирования программы необходимо создать базу данных и три таблицы:
Users – таблица с информацией о пользователях;
UrlList – хранит URL-адрес, состояние сервера на момент проверки и ID пользователя, добавившего адрес;
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-бот. У него есть несколько основных команд:
Add url – добавляет новый URL-адрес;
Remove url – удаляет данный URL-адрес;
Show – показывает список URL-адресов текущего пользователя;
Analyze url – производит анализ логов данного URL-адреса;
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.
Поделитесь своим опытом разработки на функциональных языках в комментариях!