packages feed

cachix-1.2: src/Cachix/Deploy/Log.hs

{-# LANGUAGE DerivingStrategies #-}

module Cachix.Deploy.Log where

import qualified Cachix.API.WebSocketSubprotocol as WSS
import qualified Control.Concurrent.STM.TMQueue as TMQueue
import Control.Exception.Safe (MonadMask, bracket)
import qualified Data.Aeson as Aeson
import Data.Conduit ((.|))
import qualified Data.Conduit as Conduit
import qualified Data.Conduit.Combinators as Conduit
import qualified Data.Conduit.TQueue as Conduit
import Data.Time.Clock (getCurrentTime)
import qualified Katip
import qualified Network.WebSockets as WS
import Protolude hiding (bracket, toS)
import Protolude.Conv (toS)

-- A temporary crutch while we fix up the types
type WithLog = Katip.KatipContextT IO () -> IO ()

data Options = Options
  { -- | The minimum verbosity level
    verbosity :: Verbosity,
    -- | The logging namespace, e.g. agent
    namespace :: Katip.Namespace,
    -- | The logging environment, e.g. production
    environment :: Katip.Environment
  }
  deriving stock (Show, Generic)
  deriving anyclass (Aeson.ToJSON, Aeson.FromJSON)

data Verbosity
  = Normal
  | Verbose
  deriving stock (Show, Generic)
  deriving anyclass (Aeson.ToJSON, Aeson.FromJSON)

withLog ::
  (MonadIO m, MonadMask m) =>
  Options ->
  (WithLog -> m a) ->
  m a
withLog Options {..} inner =
  let permit = case verbosity of
        Verbose -> Katip.DebugS
        Normal -> Katip.InfoS

      createLogEnv = liftIO $ do
        logEnv <- Katip.initLogEnv namespace environment
        stdoutScribe <- Katip.mkHandleScribe Katip.ColorIfTerminal stdout (Katip.permitItem permit) Katip.V2
        Katip.registerScribe "stdout" stdoutScribe Katip.defaultScribeSettings logEnv

      createContext logEnv = Katip.runKatipContextT logEnv () namespace

      closeLog = liftIO . Katip.closeScribes
   in bracket createLogEnv closeLog $ inner . createContext

-- Streaming log

type LogStream = Conduit.ConduitT ByteString Conduit.Void IO ()

-- TODO: prepend katip-like format to each line
streamLog :: WithLog -> WS.Connection -> TMQueue.TMQueue ByteString -> IO ()
streamLog logger connection queue = do
  Conduit.runConduit $
    Conduit.sourceTMQueue queue
      .| Conduit.linesUnboundedAscii
      .| logLocal logger
      .| sendLog connection

streamLine :: LogStream -> ByteString -> IO ()
streamLine logStream msg = Conduit.connect (Conduit.yield $ msg <> "\n") logStream

logLocal :: WithLog -> Conduit.ConduitT ByteString ByteString IO ()
logLocal logger =
  Conduit.mapM $ \bs -> do
    logger . Katip.logLocM Katip.DebugS . Katip.ls $ bs
    return bs

sendLog :: WS.Connection -> LogStream
sendLog connection = Conduit.mapM_ $ \bs -> do
  now <- getCurrentTime
  WS.sendTextData connection $ Aeson.encode $ WSS.Log {WSS.line = toS bs, WSS.time = now}