packages feed

instrument-0.6.0.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           System.Timeout
import           Test.Tasty
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck
-------------------------------------------------------------------------------
import           Instrument.Client
import           Instrument.Tests.Arbitrary ()
import           Instrument.Types
import           Instrument.Worker
-------------------------------------------------------------------------------

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) -> 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")]