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

17 февраля 2014

Ранее мы выяснили, как на 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 строк кода.

Метки: , .

Понравился пост? Узнайте, как можно поддержать развитие этого блога.

Также подпишитесь на RSS, Facebook, ВКонтакте, Twitter или Telegram.