← На главную

Запись метрик в Graphite, Ganglia и StatsD на Haskell

Ранее мы выяснили, как на Haskell работать с базами данных, писать REST API, генерировать и читать JSON-стримы, работать с конфигами и писать логи. Для полного счастья осталось разве что научится ходить в Graphite. После этого можно спокойно протаскивать Haskell в продакшн, пописывая на нем небольшие RESTful сервисы. Так чего же мы ждем?

В этом деле нам поможет пакет network-metrics. Он позволяет писать метрики в Graphite, а также в Ganglia и StatsD. Основные функции и типы, экспортируемые пакетом, следующие.

data SinkType = Ganglia | Graphite | Statsd | Stdout

Тип-сумма, определяющий тип «стока», то есть, места, куда стекают метрики. Другими словами, этот тип нужен для задания того, куда именно мы пишем метрики.

type Host = BS.ByteString type HostName = String newtype PortNumber = PortNum GHC.Word.Word16 instance Num PortNumber open :: SinkType -> Host -> HostName -> PortNumber -> IO AnySink

Функция open создает новый сток. Первым аргументом принимает тип стока, вторым – имя машины, на которой работает наше приложение, третьим и четвертым – хост и номер порта, на котором крутится Graphite (точнее – Carbon) или иной демон, собирающий метрики. Объявление типа AnySink находится в модуле Network.Metric.Internal и является экземпляром класса типов Sink. О его внутреннем устройстве нам думать не нужно.

close :: Sink a => a -> IO ()

Закрывает сток.

type Group = BS.ByteString type Bucket = BS.ByteString data Metric = Counter Group Bucket Integer | Timer Group Bucket Double | Gauge Group Bucket Double instance Measurable Metric

Здесь перечисляются поддерживаемые типы метрик – счетчик, таймер и мера/размер (не уверен, как в данном контексте точно переводится gauge).

push :: (Sink a, Measurable b) => a -> b -> IO ()

Пишет метрики в заданный сток.

Как видите, если не считать пары заморочек с классами типов, все довольно просто. Код небольшого тестового приложения выглядит так:

{-# LANGUAGE OverloadedStrings #-} import Network.Metric import Control.Concurrent import System.Random import Data.Pool import Network.HostName import qualified Data.ByteString.Char8 as BS import Control.Exception import Control.Monad pushLoop pool n = when (n > 0) $ do threadDelay 100000 -- 100 ms writeMetrics pool `catch` reportIOException print n pushLoop pool $ n - 1 writeMetrics pool = do counter <- randomRIO (50, 150) timer <- randomRIO (50.0, 150.0) gauge <- randomRIO (50.0, 150.0) withResource pool $ \s -> do push s $ Counter "example.store" "counter" counter push s $ Timer "example.store" "timer" timer push s $ Gauge "example.store" "gauge" gauge reportIOException :: IOException -> IO () reportIOException e = putStrLn $ "Something is wrong: " ++ show e createSink = do host <- getHostName open Graphite (BS.pack host) "localhost" 2003 main = do pool <- createPool createSink close 1 120 5 pushLoop pool 1000

Эта программа раз в 100 мс пишет случайные метрики всех трех поддерживаемых типов в работающий локально Carbon. Для того, чтобы программа переживала временную недоступность Carbon’а, используется уже знакомый нам пакет resource-pool. Для получения имени локальной машины я воспользовался пакетом hostname. Он экспортирует единственную функцию getHostName :: IO String. Пакет работает как под Windows (используется GetComputerNameExW), так и под *nix (вызывается gethostname).

Не уверен на счет Ganglia и StatsD, но в случае с Graphite никаких различий между метриками типа Counter, Timer и Gauge на данный момент пакет не делает. В этом несложно убедиться, посмотрев на рисуемые графики, или, например, сказав:

sudo tcpdump -n -q -i lo -s 0 -A 'tcp dst port 2003'

Видно, что на каждый вызов функции push пакет тупо посылает в Carbon что-то типа:

portege.example.store.timer 140.79944522 1392468876

Другими словами, никакой агрегации данных не производится. Если вам хочется отправлять метрики один раз в минуту и при этом подсчитывать суммарное количество загрузок определенной страницы или, например, максимальное время выполнения запроса к базе данных, придется написать соответствующий код самостоятельно. Готового пакета для решения этой проблемы на Hackage мне найти не удалось.

Вот и все, о чем я хотел вам сегодня поведать. Исходники к этой заметке вы найдете в этом архиве. Инструкция по их сборке находится здесь.

Дополнение: В последней версии EKG появилась возможность записи метрик в statsd, а следовательно и во все поддерживаемые им бэкенды, в том числе Graphite. Кроме того, чтобы интегрировать EKG с другими сервисами, например, напрямую с Graphite, нужно написать всего лишь ~120 строк кода.