packages feed

honeycomb-0.0.0.1: src/Honeycomb.hs

{-# LANGUAGE RecordWildCards #-}
{-|
Module      : Honeycomb
Description : A simple interface to send events to Honeycomb.
Copyright   : (c) Ian Duncan, 2021
License     : BSD-3
Maintainer  : ian@iankduncan.com
Stability   : unstable
Portability : Portable

Warning, not all configuration options actually do what they claim yet.
-}
module Honeycomb
  ( 
  -- * Initializing and shutting down a 'HoneycombClient'
    HoneycombClient
  , initializeHoneycomb
  , Config.config
  , shutdownHoneycomb
  -- * Sending events
  , event
  , Event(..)
  , send
  -- * Embedding a HoneycombClient into larger applications
  , MonadHoneycomb
  , HasHoneycombClient(..)
  ) where

import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class
import Data.HashMap.Strict as S
import Data.Maybe
import System.Random.MWC
import qualified Honeycomb.Config as Config
import Honeycomb.Types
import Honeycomb.Client.Internal
import qualified Honeycomb.API.Events as API
import qualified Honeycomb.API.Types as API
import Network.HTTP.Client.TLS
import UnliftIO.Async hiding (atomically)
import UnliftIO
import Control.Monad.Reader
import Control.Concurrent.STM (retry)
import Control.Concurrent.STM.TBQueue hiding (newTBQueueIO)
import Control.Concurrent
import Lens.Micro ((%~), (^.), (&))
import Lens.Micro.Extras (view)

initializeHoneycomb :: MonadIO m => Config.Config -> m HoneycombClient
initializeHoneycomb conf = liftIO $ do
  putStrLn "Initialize honeycomb client"
  rand <- liftIO createSystemRandom
  buf <- liftIO $ newTBQueueIO (fromIntegral $ Config.pendingQueueSize conf)
  sendThreadCount <- fmap (max 1) $ if Config.sendThreads conf == 0
    then liftIO (fmap fromIntegral getNumCapabilities)
    else pure $ fromIntegral $ Config.sendThreads conf
  liftIO $ print ("sendThreadCount"::String, sendThreadCount)
  -- TODO this will lose some events upon cancellation, so we need to handle that by properly
  -- flushing everything.
  innerWorkers <- liftIO $ replicateM (fromIntegral $ Config.sendThreads conf) $ async $ do
    putStrLn "Booting worker thread"
    forever $ do
      actions <- mask_ $ liftIO $ do
        items <- atomically $ flushTBQueue buf
        -- TODO do something better than exception printing
        handle (\e -> print (e :: SomeException) *> throwIO e) $ sequence_ items
        pure items
      -- A little hack to retry outside of mask so that way we can cancel in between outbound calls
      case actions of
        [] -> atomically $ void $ peekTBQueue buf
        _ -> pure ()
  pure $ HoneycombClient conf rand buf innerWorkers

shutdownHoneycomb :: MonadIO m => HoneycombClient -> m ()
shutdownHoneycomb = mapM_ cancel . clientWorkers

event :: Event
event = Event
  { fields = S.empty
  , teamWriteKey = Nothing
  , dataset = Nothing
  , apiHost = Nothing
  , sampleRate = Nothing
  , timestamp = Nothing
  }

class ToEventField a where
class ToEventFields a where

send :: (MonadIO m, HasHoneycombClient env) => env -> Event -> m ()
send hasC e = do
  let c@HoneycombClient{..} = hasC ^. honeycombClientL
      specifiedSampleRate = sampleRate e <|> Config.sampleRate clientConfig
  (shouldSend, _sampleVal) <- case specifiedSampleRate of
    Nothing -> pure (True, 0)
    Just 1 -> pure (True, 0)
    Just n -> liftIO $ do
      x <- uniformR (1, n) clientGen
      pure (1 == x, x)
  when shouldSend $ do
    let event_ = API.Event specifiedSampleRate (timestamp e) (fields e)
        localOptions = honeycombClientL %~ (\c -> c { clientConfig = replaceDataset $ replaceHost $ replaceWriteKey clientConfig })
        blockingEvent = runReaderT (API.sendEvent event_) (c & localOptions)
    liftIO $ if Config.sendBlocking clientConfig
      then blockingEvent
      else atomically $ writeTBQueue clientEventBuffer blockingEvent
  where
    replaceDataset :: Config.Config -> Config.Config
    replaceDataset c' = maybe c' (\ds -> c' { Config.defaultDataset = ds }) $ dataset e
    replaceHost :: Config.Config -> Config.Config
    replaceHost c' = maybe c' (\h -> c' { Config.apiHost = h }) $ apiHost e
    replaceWriteKey :: Config.Config -> Config.Config
    replaceWriteKey c' = maybe c' (\k -> c' { Config.teamWritekey = k }) $ teamWriteKey e