packages feed

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