instrument-0.6.1.0: test/src/Instrument/Tests/Client.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Instrument.Tests.Client
( tests,
)
where
-------------------------------------------------------------------------------
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception.Safe (Exception, throwM, tryAny)
import Control.Monad
import Control.Monad.IO.Class
import Data.Default
import Data.List (find)
import qualified Data.Map as M
import Data.Monoid as Monoid
import Database.Redis
-------------------------------------------------------------------------------
import Instrument.Client
import Instrument.Tests.Arbitrary ()
import Instrument.Types
import Instrument.Worker
import System.Timeout
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
-------------------------------------------------------------------------------
tests :: TestTree
tests =
testGroup
"Instrument.Client"
[ withRedisCleanup $ testCase "queue bounding works" . queue_bounding_test,
withRedisCleanup $ testCase "multiple keys eventually get flushed" . multi_key_test,
withRedisCleanup $ testCaseSteps "also times exceptions" . time_test,
timerMetricNameTests
]
-------------------------------------------------------------------------------
queue_bounding_test :: IO Connection -> IO ()
queue_bounding_test mkConn = do
conn <- mkConn
instr <- initInstrument redisCI icfg
agg <- newEmptyMVar
replicateM_ 2 (sampleI key DoNotAddHostDimension dims 1 instr >> sleepFlush)
-- redis queue slots now full of the above aggregate
-- bounds will drop these
sampleI key DoNotAddHostDimension dims 100 instr
sleepFlush
let worker = work conn 1 (AggProcess defAggProcessConfig (liftIO . putMVar agg))
withAsync worker $ \_ -> do
Aggregated {aggPayload = AggStats Stats {..}} <- takeMVar agg
assertEqual
"throws away newer data exceeding bounds"
(2, 1, 1, 2)
(scount, smin, smax, ssum)
-------------------------------------------------------------------------------
sleepFlush :: IO ()
sleepFlush = threadDelay 1100000
data TimeException
= TimeException
deriving (Show)
instance Exception TimeException
-------------------------------------------------------------------------------
time_test :: IO Connection -> (String -> IO a) -> IO ()
time_test mkConn step = do
conn <- mkConn
instr <- initInstrument redisCI icfg
aggsRef <- newTVarIO []
let toMetric resE =
case resE of
Left _ -> ("instrument-test-timeex-error", DoNotAddHostDimension, Monoid.mempty)
Right _ -> ("instrument-test-timeex", DoNotAddHostDimension, Monoid.mempty)
void . tryAny . timeExI toMetric instr $ throwM TimeException
timeExI toMetric instr $ pure ()
void . tryAny
. timeI
"instrument-test-time-error"
DoNotAddHostDimension
Monoid.mempty
instr
$ throwM TimeException
timeI
"instrument-test-time"
DoNotAddHostDimension
Monoid.mempty
instr
$ pure ()
sleepFlush
let collectAggs = work conn 1 (AggProcess defAggProcessConfig (\agg -> liftIO (atomically (modifyTVar' aggsRef (agg :)))))
withAsync collectAggs $ \worker -> do
maggs <- timeout 2000000 $
atomically $ do
aggs <- readTVar aggsRef
check (length aggs == 3)
return aggs
cancel worker
case maggs of
Nothing -> assertFailure "Waited 2 seconds and never received any aggs!"
Just aggs -> do
let getCount payload =
case payload of
(AggStats stats) -> scount stats
(AggCount n) -> fromIntegral n
countMatches name n = do
void $ step name
case filter ((== MetricName ("time." <> name)) . aggName) aggs of
[agg] -> getCount (aggPayload agg) @?= n
_ -> assertFailure (name <> "has multiple aggregations")
countMatches "instrument-test-timeex" 1
countMatches "instrument-test-timeex-error" 1
countMatches "instrument-test-time" 1
void $ step "instrument-test-time-error"
length (filter ((== MetricName "time.instrument-test-time-error") . aggName) aggs) @?= 0
-------------------------------------------------------------------------------
multi_key_test :: IO Connection -> IO ()
multi_key_test mkConn = do
conn <- mkConn
instr <- initInstrument redisCI icfg
aggsRef <- newTVarIO []
sampleI "instrument-test1" DoNotAddHostDimension Monoid.mempty 1 instr
sampleI "instrument-test2" DoNotAddHostDimension mempty 2 instr
sleepFlush
let collectAggs = work conn 1 (AggProcess defAggProcessConfig (\agg -> liftIO (atomically (modifyTVar' aggsRef (agg :)))))
withAsync collectAggs $ \worker -> do
maggs <- timeout 2000000 $
atomically $ do
aggs <- readTVar aggsRef
check (length aggs >= 2)
return aggs
cancel worker
case maggs of
Nothing -> assertFailure "Waited 2 seconds and never received any aggs!"
Just aggs -> do
length aggs @?= 2
let findAgg n expectedMean = do
let magg = find ((== MetricName n) . aggName) aggs
case magg of
Nothing -> assertFailure ("Expected to find agg with name " <> n <> " but could not")
Just (Aggregated {aggPayload = AggStats (Stats {smean = actualMean})}) -> actualMean @?= expectedMean
Just _ -> assertFailure ("Expected agg with name " <> n <> " to contain Stats but it did not.")
findAgg "instrument-test1" 1.0
findAgg "instrument-test2" 2.0
-------------------------------------------------------------------------------
timerMetricNameTests :: TestTree
timerMetricNameTests =
testGroup
"timerMetricName"
[ testProperty "is idempotent" $ \mn ->
let r1 = timerMetricName mn
r2 = timerMetricName r1
in r1 === r2,
testProperty "is idempotent for timers" $ \(MetricName mnBase) ->
let mn = MetricName (timerMetricNamePrefix <> mnBase)
r1 = timerMetricName mn
r2 = timerMetricName r1
in r1 === r2,
testCase "adds time. prefix" $ do
timerMetricName (MetricName "foo") @?= MetricName "time.foo"
]
-------------------------------------------------------------------------------
withRedisCleanup :: (IO Connection -> TestTree) -> TestTree
withRedisCleanup = withResource (connect redisCI) cleanup
where
cleanup conn = void $
runRedis conn $ do
ks <- either mempty id <$> smembers packetsKey
_ <- del (packetsKey : ks)
quit
-------------------------------------------------------------------------------
redisCI :: ConnectInfo
redisCI = defaultConnectInfo
icfg :: InstrumentConfig
icfg = def {redisQueueBound = Just 2}
key :: MetricName
key = MetricName "instrument-test"
dims :: Dimensions
dims = M.fromList [(DimensionName "server", DimensionValue "app1")]