borel-0.18.0: lib/Borel.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
module Borel
( -- * Metric
Metric(..)
, cpu, block, ssd, diskReads, diskWrites, neutronIn, neutronOut
, ipv4, vcpus, memory, snapshot, image
, mkInstance
-- * Unit of measurement
, UOM(..), BaseUOM(..), Prefix(..)
, nanosec, byte, megabyte, gigabyte
, countCPU, countVCPU, countInstance, countIP
, convert
, nanosecToSec, nanosecToHour, byteToGigabyte
-- * BorelConfig
, BorelConfig(..)
, mkBorelConfig, parseBorelConfig, loadBorelConfig
, allInstances, allMetrics
, paramBorelConfig, paramFlavorMap, paramOrigins, paramMarquiseURI, paramChevalierURI
-- * Query arguments
, BorelEnv
, paramStart, paramEnd, paramMetrics, paramTID
, TenancyID(..)
-- * Query results
, ResponseItem(..)
, mkItem
, respResource, respResourceID, respVal
, uomVal
-- * Running
, run, runF
, BorelError(..)
) where
import Control.Applicative
import Control.Concurrent.Async
import qualified Control.Exception as E
import Control.Lens
import Control.Monad.Reader
import Data.Set (Set)
import qualified Data.Set as S
import Pipes hiding (Proxy)
import qualified Pipes as P
import Pipes.Concurrent
import qualified Pipes.Prelude as P
import Pipes.Safe
import System.Log.Logger
-- friends
import Ceilometer.Client
import Vaultaire.Types
-- family
import Borel.Ceilometer
import Borel.Chevalier
import Borel.Marquise
import Borel.Types
--------------------------------------------------------------------------------
-- | Metrics that must be queried as a common Ceilometer resource are grouped together,
-- others are left as-is.
--
-- e.g. [instance-1, instance-2,..]
--
groupMetrics :: Set Metric -- ^ All available instances
-> Set Metric -- ^ Metrics to be grouped
-> Set GroupedMetric
groupMetrics instances metrics =
let (allfs, nonfs) = bimap S.toList (S.map pure) $ S.partition (`S.member` instances) metrics
in case allfs of
[] -> nonfs
xs -> S.insert xs nonfs
--------------------------------------------------------------------------------
-- | Leverages Chevalier, Marquise and Ceilometer
-- to find, fetch, decode and aggregate data for an OpenStack tenancy.
--
run :: (MonadSafe m, Applicative m)
=> BorelConfig -- ^ Borel config, e.g. contains Chevalier/Marquise URI.
-> Set Metric -- ^ Metrics requested
-> TenancyID -- ^ OpenStack tenancy (can lead to multiple metrics)
-> TimeStamp -- ^ Start time
-> TimeStamp -- ^ End time
-> Producer ResponseItem m ()
run conf ms tid s e = runBorel conf ms tid s e (query >-> P.map snd)
-- | Acts like a functor over the produced results.
-- This is not a Haskell functor, it's a functor between Hask and cat of Proxy composition.
--
runF :: (MonadSafe m, Applicative m)
=> ((Metric, ResponseItem) -> a)
-> BorelConfig -- ^ Borel config, e.g. contains Chevalier/Marquise URI.
-> Set Metric -- ^ Metrics requested
-> TenancyID -- ^ OpenStack tenancy (can lead to multiple metrics)
-> TimeStamp -- ^ Start time
-> TimeStamp -- ^ End time
-> Producer a m ()
runF f conf ms tid s e = runBorel conf ms tid s e (query >-> P.map f)
query :: (Applicative m, MonadSafe m, MonadIO m)
=> Producer (Metric, ResponseItem) (BorelS m) ()
query = do
liftIO $ updateGlobalLogger "borel" (setLevel DEBUG)
params <- ask
(outputWork, inputWork, sealWork) <- liftIO . spawn' $ bounded 1
(outputRes, inputRes, sealRes) <- liftIO . spawn' $ bounded 1
let grouped = groupMetrics
( params ^. paramBorelConfig . allInstances)
( params ^. paramMetrics)
defaultFilters = Filters billableStatus
producer
:: (MonadIO m, MonadSafe m)
=> Producer (GroupedMetric, Origin, Address, SourceDict) m ()
producer = P.enumerate
[ (metrics, org, addr, sd)
| metrics <- Select $ P.each grouped
, (org, addr, sd) <- Select $ chevalier params (metrics, params ^. paramTID)
]
worker
:: (MonadIO m, MonadSafe m)
=> Pipe (GroupedMetric, Origin, Address, SourceDict)
(Metric, ResponseItem)
m
()
worker = forever $ do
let flavors = params ^. paramBorelConfig . paramFlavorMap
start = params ^. paramStart
end = params ^. paramEnd
(metrics, org, addr, sd) <- await
results <- ceilometer flavors metrics
(Env flavors sd defaultFilters start end)
(marquise params (metrics, org, addr))
forM_ results $ \result -> yield (fst result, mkItem sd result)
-- This hierarchy is necessary to close the buffers in case of an error.
pollWorkers <- liftIO . async $ (do
workers <- replicateM 8 . async . runSafeT . runEffect $
fromInput inputWork >-> worker >-> toOutput outputRes
mapM_ link workers
mapM_ wait workers) `E.finally` (atomically $ sealWork >> sealRes)
liftIO $ link pollWorkers
pollProducer <- liftIO . async $
(runSafeT . runEffect $ producer >-> toOutput outputWork)
`E.finally` atomically sealWork
liftIO $ link pollProducer
fromInput inputRes
liftIO $ wait pollProducer
liftIO . atomically $ sealRes
liftIO $ wait pollWorkers