packages feed

batchd-0.1.1.0: src/Batchd/Daemon/Monitoring.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Batchd.Daemon.Monitoring 
  (
    WaiMetrics,
    setupMetrics,
    metricsDumper, metricsCleaner,
    getWaiMetricsMiddleware,
    getCurrentMetrics,
    metricRecordToJsonTree,
    metricRecordToJsonPlain,
    sampleToJsonPlain,
    Metrics.counter,
    Metrics.gauge,
    Metrics.timed,
    timedN,
    Metrics.distribution,
    Metrics.label
  ) where

import Control.Monad
import Control.Monad.State
import Control.Concurrent
import qualified Control.Monad.Metrics as Metrics
import Control.Monad.Catch as MC
import Lens.Micro
import Data.Time
import qualified Data.Vector as V
import Data.Aeson as Aeson
import Data.Aeson.Types as Aeson
import qualified Data.Text as T
import Data.Text.Format.Heavy
import qualified Data.HashMap.Lazy as H
import qualified Data.HashMap.Strict as M
import Database.Persist
import qualified System.Metrics as EKG
import qualified System.Metrics.Distribution as EKG
import qualified Network.Wai as Wai
import Network.Wai.Metrics

import Batchd.Core.Daemon.Logging
import Batchd.Common.Types
import Batchd.Common.Data
import Batchd.Daemon.Types

setupMetrics :: Daemon ()
setupMetrics = do
  store <- liftIO EKG.newStore
  cfg <- askConfig
  when (mcGcMetrics $ dbcMetrics cfg) $ do
      liftIO $ EKG.registerGcMetrics store
  mbWaiMetrics <- if mcHttpMetrics $ dbcMetrics cfg
                    then Just `fmap` (liftIO $ registerNamedWaiMetrics "batchd" store)
                    else return Nothing
  metrics <- liftIO $ Metrics.initializeWith store
  Daemon $ lift $ modify $ \st -> st {ciMetrics = Just metrics, ciWaiMetrics = mbWaiMetrics}

instance Metrics.MonadMetrics Daemon where
  getMetrics = do
    ci <- askConnectionInfo
    case ciMetrics ci of
      Nothing -> fail $ "Metrics are not initialized yet!"
      Just m -> return m

getWaiMetricsMiddleware :: Daemon (Maybe Wai.Middleware)
getWaiMetricsMiddleware = do
  ci <- askConnectionInfo
  case ciWaiMetrics ci of
    Nothing -> return Nothing
    Just wm -> return $ Just $ metrics wm

getCurrentMetrics :: Maybe T.Text -> Daemon EKG.Sample
getCurrentMetrics mbPrefix = do
    metrics <- Metrics.getMetrics
    let store = metrics ^. Metrics.metricsStore
    sample <- liftIO $ EKG.sampleAll store
    let good = case mbPrefix of
                 Just prefix -> H.filterWithKey (\name _ -> prefix `T.isPrefixOf` name) sample
                 Nothing -> sample
    return good

metricsDumper :: Daemon ()
metricsDumper = do
  cfg <- askConfig
  let mode = dbcDaemonMode cfg
  let timeout = mcDumpTimeout $ dbcMetrics cfg
  forever $ do
    liftIO $ threadDelay $ timeout * 1000 * 1000
    now <- liftIO $ getCurrentTime
    sample <- getCurrentMetrics Nothing
    r <- runDB $ do
            forM_ (H.toList sample) $ \(name, value) -> do
              let ok = case mcStorePrefixOnly $ dbcMetrics cfg of
                         Nothing -> True
                         Just prefix -> prefix `T.isPrefixOf` name
              when ok $ do
                  let record = mkRecord mode now name value
                  insert_ record
    case r of
      Left err -> $reportError "Can't insert metric record to DB: {}" (Single $ show err)
      Right _ -> return ()

mkRecord :: DaemonMode -> UTCTime -> T.Text -> EKG.Value -> MetricRecord
mkRecord mode time name value =
    case value of
      EKG.Counter n  -> (emptyRecord Counter) {metricRecordValue = Just n}
      EKG.Gauge n    -> (emptyRecord Gauge)   {metricRecordValue = Just n}
      EKG.Label text -> (emptyRecord Label)   {metricRecordText = Just text}
      EKG.Distribution st ->
        (emptyRecord Distribution) {
          metricRecordMean = Just $ EKG.mean st,
          metricRecordVariance = Just $ EKG.variance st,
          metricRecordCount = Just $ EKG.count st,
          metricRecordSum = Just $ EKG.sum st,
          metricRecordMin = Just $ EKG.min st,
          metricRecordMax = Just $ EKG.max st
        }
  where
    emptyRecord kind = MetricRecord name time mode kind Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing

metricsCleaner :: Daemon ()
metricsCleaner = do
  cfg <- askConfig
  let days = scMetricRecords $ dbcStorage cfg
  forever $ do
    liftIO $ threadDelay $ 60 * 60 * 1000*1000
    now <- liftIO $ getCurrentTime
    let delta = fromIntegral $ days * 24 * 3600
    let edge = addUTCTime (negate delta) now
    r <- runDB $ do
           deleteWhere [MetricRecordTime <. edge]
    case r of
      Left err -> $reportError "Can't clean metrics history: {}" (Single $ show err)
      Right _ -> return ()

-- metricValueToJson :: MetricRecord -> Aeson.Value
-- metricValueToJson r =
--   case metricRecordKind r of
--     Counter -> object ["type" .= ("c" :: T.Text), "val" .= metricRecordValue r]
--     Gauge   -> object ["type" .= ("g" :: T.Text), "val" .= metricRecordValue r]
--     Label   -> object ["type" .= ("l" :: T.Text), "val" .= metricRecordText r]
--     Distribution ->
--       object [
--         "type" .= ("d" :: T.Text), 
--         "mean" .= metricRecordMean r,
--         "variance" .= metricRecordVariance r,
--         "count" .= metricRecordCount r,
--         "sum" .= metricRecordSum r,
--         "min" .= metricRecordMin r,
--         "max" .= metricRecordMax r
--       ]

metricRecordToJsonPlain :: MetricRecord -> Aeson.Value
metricRecordToJsonPlain r = toJSON r

sampleToJsonPlain :: UTCTime -> EKG.Sample -> Aeson.Value
sampleToJsonPlain time sample = Aeson.Array $ V.fromList $ map convert $ H.toList sample
  where
    convert (name, value) = toJSON $ mkRecord Manager time name value

metricRecordToJsonTree :: MetricRecord -> Aeson.Value
metricRecordToJsonTree r =
  let metricValue = toJSON r

      build :: Aeson.Value -> T.Text -> Aeson.Value -> Aeson.Value
      build m name val = go m (T.splitOn "." name) val

      go :: Aeson.Value -> [T.Text] -> Aeson.Value -> Aeson.Value
      go (Aeson.Object m) [str] val      = Aeson.Object $ M.insert str val m
      go (Aeson.Object m) (str:rest) val = case M.lookup str m of
          Nothing -> Aeson.Object $ M.insert str (go Aeson.emptyObject rest val) m
          Just m' -> Aeson.Object $ M.insert str (go m' rest val) m
      go v _ _                        = error $ "metricToRecordJson.go: unexpected: " ++ show v

  in  build Aeson.emptyObject (metricRecordName r) metricValue

timedN :: (MonadIO m, Metrics.MonadMetrics m, MonadMask m) => [T.Text] -> m a -> m a
timedN = Metrics.timedList Metrics.Seconds