packages feed

otel-effectful-1.0.0: test/Effectful/OpenTelemetry/Exporter/Grafana/Polling.hs

module Effectful.OpenTelemetry.Exporter.Grafana.Polling where

import Control.Monad (unless)
import Data.Either (isLeft)
import Data.Functor (void)
import Data.Maybe (fromMaybe)
import Effectful
import Effectful.Exception (bracket, catchSync)
import Effectful.HUnit (HUnit, assertFailure)
import Effectful.Hspec
import Effectful.HttpClient (httpLbs, parseRequest_, runHttpClientTls)
import Effectful.Retry
    ( Retry
    , capDelay
    , constantDelay
    , exponentialBackoff
    , limitRetriesByCumulativeDelay
    , retrying
    )
import Effectful.Timeout (Timeout, timeout)
import GHC.Stack (HasCallStack)
import Network.HTTP.Client (responseStatus)
import Network.HTTP.Types.Status (statusIsSuccessful)
import Network.Simple.TCP (closeSock, connectSock)
import Network.URI (URI (..), URIAuth (..))
import Prelude

pollRetrying
    :: forall es a
     . (Retry :> es, Timeout :> es)
    => Eff es (Either String a)
    -> Eff es (Either String a)
pollRetrying =
    retrying
        ( limitRetriesByCumulativeDelay 30_000_000
            . capDelay 2_000_000
            $ exponentialBackoff 250_000
        )
        (const $ pure . isLeft)
        . const
        . attempt
  where
    attempt :: Eff es (Either String a) -> Eff es (Either String a)
    attempt = fmap (fromMaybe $ Left "attempt timed out") . timeout 5_000_000

pollOrFail
    :: (HasCallStack, Retry :> es, HUnit :> es, Timeout :> es)
    => Eff es (Either String a)
    -> (a -> Eff es r)
    -> Eff es r
pollOrFail fetch onFound =
    pollRetrying fetch >>= \case
        Left reason -> assertFailure reason
        Right a -> onFound a

-- | Skip the surrounded example via 'pendingWith' when nothing is accepting
-- connections, for a collector with no @\/ready@ endpoint of its own.
withListening
    :: (IOE :> es, Hspec :> es)
    => String
    -> URI
    -> Eff es ()
    -> Eff es ()
withListening backend backendUri body = do
    listening <- isListening backendUri
    if listening
        then body
        else pendingWith (backend <> " is not listening at " <> show backendUri)

-- | Like 'withListening', but as a statement rather than a wrapper
checkListening :: (IOE :> es, Hspec :> es) => String -> URI -> Eff es ()
checkListening backend backendUri = do
    listening <- isListening backendUri
    unless listening $ pendingWith (backend <> " is not listening at " <> show backendUri)

-- | 'checkListening', plus waiting for the backend to report readiness.
checkReady :: (IOE :> es, Hspec :> es, Retry :> es) => String -> URI -> Eff es ()
checkReady backend backendUri = do
    checkListening backend backendUri
    waitReady backendUri

isListening :: (IOE :> es) => URI -> Eff es Bool
isListening baseUri = case baseUri of
    URI{uriAuthority = Just URIAuth{uriRegName, ..}}
        | ':' : port <- uriPort -> do
            (bracket (connectSock uriRegName port) (closeSock . fst) . const $ pure True)
                `catchSync` const (pure False)
    _ -> pure False

-- | Query the backend readiness via the standard Grafana-stack @\/ready@ endpoint.
waitReady :: (IOE :> es, Retry :> es) => URI -> Eff es ()
waitReady baseUri = do
    let url = show baseUri{uriPath = "/ready"}
    void
        . retrying
            (limitRetriesByCumulativeDelay 30_000_000 $ constantDelay 1_000_000)
            (const $ pure . not . statusIsSuccessful)
        . const
        . runHttpClientTls
        $ responseStatus <$> httpLbs (parseRequest_ url)