packages feed

prodapi-0.1.0.0: src/Prod/Discovery.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Module for performing service (endpoints) discovery.
module Prod.Discovery where

import Control.Concurrent (threadDelay)
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time.Clock (UTCTime, getCurrentTime)

import Prod.Background (BackgroundVal, MicroSeconds, background, readBackgroundVal)
import qualified Prod.Background
import Prod.Tracer (Tracer (..), contramap)
import Prometheus (Counter, Gauge, Label3, Vector)
import qualified Prometheus as Prometheus
import System.Process.ByteString (readProcessWithExitCode)

data Track a = BackgroundTrack (Prod.Background.Track (Result a))
    deriving (Show, Functor)

data Result a
    = NotAsked
    | Asked UTCTime
    | Found UTCTime a
    deriving (Show, Functor)

toMaybe :: Result a -> Maybe a
toMaybe (Found _ a) = Just a
toMaybe _ = Nothing

data Discovery a = Discovery (BackgroundVal (Result a))
    deriving (Functor)

readCurrent :: Discovery a -> IO (Result a)
readCurrent (Discovery b) = readBackgroundVal b

type Host = Text

data DNSTrack a = DNSTrack Text Host (Track a)
    deriving (Show, Functor)

dnsA :: Tracer IO (DNSTrack [Host]) -> Host -> IO (Discovery [Host])
dnsA tracer hostname = dig (contramap (DNSTrack hostname "A") tracer) "A" $ Text.unpack hostname

dnsAAAA :: Tracer IO (DNSTrack [Host]) -> Host -> IO (Discovery [Host])
dnsAAAA tracer hostname = dig (contramap (DNSTrack hostname "AAAA") tracer) "AAAA" $ Text.unpack hostname

dig :: Tracer IO (Track [Host]) -> String -> String -> IO (Discovery [Host])
dig tracer typ target = cmdOut tracer "dig" ["+short", typ, target] "" tenSecs [] replaceHosts trigger
  where
    replaceHosts :: [Host] -> ByteString -> [Host]
    replaceHosts _ = parseHosts

    trigger :: [Host] -> [Host] -> IO ()
    trigger _ xs = do
        Prometheus.withLabel
            dnsDiscoveryCounter
            promLabels
            Prometheus.incCounter
        Prometheus.withLabel
            dnsDiscoveryGauge
            promLabels
            (flip Prometheus.setGauge (fromIntegral $ length xs))

    promLabels :: Label3
    promLabels = ("dig", Text.pack typ, Text.pack target)

    parseHosts :: ByteString -> [Host]
    parseHosts = Text.lines . Text.decodeUtf8

    tenSecs :: Maybe (MicroSeconds Int)
    tenSecs = Just 10_000_000

{-# NOINLINE dnsDiscoveryGauge #-}
dnsDiscoveryGauge :: Vector Label3 Gauge
dnsDiscoveryGauge =
    Prometheus.unsafeRegister $
        Prometheus.vector ("m", "type", "host") $
            Prometheus.gauge (Prometheus.Info "prodapi_dns_discovery_results" "")

{-# NOINLINE dnsDiscoveryCounter #-}
dnsDiscoveryCounter :: Vector Label3 Counter
dnsDiscoveryCounter =
    Prometheus.unsafeRegister $
        Prometheus.vector ("m", "type", "host") $
            Prometheus.counter (Prometheus.Info "prodapi_dns_discoveries" "")

cmdOut ::
    forall a.
    Tracer IO (Track a) ->
    String -> -- program to run
    [String] -> -- arguments to the program to run
    ByteString -> -- input submitted to the program
    Maybe (MicroSeconds Int) -> -- delay between invocations
    a ->
    (a -> ByteString -> a) ->
    (a -> a -> IO ()) ->
    IO (Discovery a)
cmdOut tracer cmd args input ms st0 update trigger =
    Discovery <$> background (contramap BackgroundTrack tracer) st0 NotAsked run
  where
    run :: a -> IO (Result a, a)
    run st0 = do
        maybe (pure ()) threadDelay ms
        out <- runCmd
        let st1 = update st0 out
        now <- getCurrentTime
        seq st1 $ trigger st0 st1
        pure $ (Found now st1, st1)

    runCmd :: IO ByteString
    runCmd = (\(_, x, _) -> x) <$> readProcessWithExitCode cmd args input