packages feed

utxorpc-server-0.0.1.0: src/Utxorpc/Logged.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}

module Utxorpc.Logged
  ( UtxorpcServiceLogger (..),
    RequestLogger,
    ReplyLogger,
    ServerStreamLogger,
    ServerStreamEndLogger,
    loggedUnary,
    loggedUnaryHandler,
    loggedSStream,
    loggedSStreamHandler,
  )
where

import Control.Monad.IO.Class (MonadIO, liftIO)
import qualified Data.ByteString.Char8 as BS
import Data.UUID (UUID)
import Data.UUID.V4 (nextRandom)
import Network.GRPC.HTTP2.Encoding (GRPCInput, GRPCOutput)
import Network.GRPC.HTTP2.Types (IsRPC (..))
import Network.GRPC.Server (ServiceHandler, UnaryHandler)
import Network.GRPC.Server.Handlers.Trans (ServerStream (..), ServerStreamHandler, serverStream, unary)
import Network.Wai (Request (..))

-- | A record of logging functions that runs in the same monad as
-- the request handlers. Monadic state is passed along throughout the
-- lifecycle of responding to a request. This means that changes to the
-- monadic state in the request logger is seen by the stream logger, stream
-- handler and logger, and reply logger. An `unlift` function to run the monad
-- in IO is provided to @'runUtxorpc'@.
data UtxorpcServiceLogger m = UtxorpcServiceLogger
  { requestLogger :: RequestLogger m,
    replyLogger :: ReplyLogger m,
    serverStreamLogger :: ServerStreamLogger m,
    serverStreamEndLogger :: ServerStreamEndLogger m
  }

-- | Log incoming requests.
type RequestLogger m =
  forall i.
  (Show i) =>
  -- | The RPC path
  BS.ByteString ->
  -- | Request metadata
  Request ->
  -- | A UUID generated for this request and passed to stream and reply loggers.
  UUID ->
  -- | The request message
  i ->
  m ()

-- | Log outgoing replies.
type ReplyLogger m =
  forall o.
  (Show o) =>
  -- | The RPC path
  BS.ByteString ->
  -- | Request metadata
  Request ->
  -- Generated for the request that caused this reply
  UUID ->
  -- | The reply message
  o ->
  m ()

-- | Log outgoing server stream messages.
type ServerStreamLogger m =
  forall o.
  (Show o) =>
  -- | The RPC path
  BS.ByteString ->
  -- | Request metadata
  Request ->
  -- | The UUID generated for the request that generated this stream,
  -- and the 0-based index of the message in the stream.
  (UUID, Int) ->
  -- | The stream message
  o ->
  m ()

-- | Log the end of a server stream.
type ServerStreamEndLogger m =
  -- | The RPC path
  BS.ByteString ->
  -- | Request metadata
  Request ->
  -- | The UUID generated for the request that generated this stream,
  -- and the 0-based index of the message in the stream.
  (UUID, Int) ->
  m ()

-- | Creates a ServiceHandler that warp-grpc uses to handle requests
loggedUnary ::
  (MonadIO m, GRPCInput r i, GRPCOutput r o, Show i, Show o) =>
  -- | An `unlift` function for the logger and handler monad
  -- Monad state is carried through from request logger, to handler, to reply logger,
  -- So changes to the monad state in the request logger is seen by the handler and reply logger.
  (forall x. m x -> IO x) ->
  -- | The RPC
  r ->
  -- | Generate a reply from request metadata and a proto Message
  UnaryHandler m i o ->
  -- | A logger that runs in the same monad as the handlers
  Maybe (UtxorpcServiceLogger m) ->
  ServiceHandler
loggedUnary unlift rpc handler maybeLogger =
  unary unlift rpc $ maybe handler loggedHandler maybeLogger
  where
    -- Generate UUID here for easier testing of `loggedUnaryHandler`
    loggedHandler logger req msg = do
      uuid <- liftIO nextRandom
      loggedUnaryHandler rpc handler uuid logger req msg

loggedUnaryHandler ::
  (MonadIO m, Show i, Show o, IsRPC r) =>
  r ->
  UnaryHandler m i o ->
  UUID ->
  UtxorpcServiceLogger m ->
  UnaryHandler m i o
loggedUnaryHandler
  rpc
  handler
  uuid
  UtxorpcServiceLogger {requestLogger, replyLogger}
  req
  msg =
    do
      requestLogger (path rpc) req uuid msg
      reply <- handler req msg
      replyLogger rpcPath req uuid reply
      return reply
    where
      rpcPath = path rpc

-- | Creates a ServiceHandler that warp-grpc uses to handle stream requests
loggedSStream ::
  (MonadIO m, GRPCInput r i, GRPCOutput r o, Show i, Show o) =>
  -- | An unlift function for the logger and handler
  -- Monadic state changes are passed from request logger to stream logger and handlers, and so on.
  -- So changes to the monadic state in the request logger are seen by the handler and other loggers.
  (forall x. m x -> IO x) ->
  -- | The RPC
  r ->
  -- | A function that, given request metadata and a protobuf Message,
  -- generates an initial stream state and a function that folds over the stream state to produce
  -- a stream of messages.
  ServerStreamHandler m i o a ->
  -- | A logger that runs in the same monad as the handler
  Maybe (UtxorpcServiceLogger m) ->
  ServiceHandler
loggedSStream unlift rpc handler Nothing = serverStream unlift rpc handler
loggedSStream unlift rpc handler (Just logger) =
  serverStream unlift rpc loggedHandler
  where
    loggedHandler req msg = do
      uuid <- liftIO nextRandom
      loggedSStreamHandler rpc handler uuid logger req msg

loggedSStreamHandler ::
  (MonadIO m, IsRPC r, Show i, Show o) =>
  r ->
  ServerStreamHandler m i o a ->
  UUID ->
  UtxorpcServiceLogger m ->
  ServerStreamHandler m i o (a, Int)
loggedSStreamHandler
  rpc
  handler
  uuid
  UtxorpcServiceLogger {requestLogger, serverStreamLogger, serverStreamEndLogger}
  req
  msg = do
    -- Log request
    requestLogger rpcPath req uuid msg
    -- Get initial stream state and stream output function
    (initStreamState, ServerStream {serverStreamNext}) <- handler req msg
    -- Wrap stream output function with logging
    let loggedStreamNext = mkLoggedStreamNext serverStreamNext
    -- The unwrapped handler returns the initial stream state and stream output function
    -- We add initial log state and return the wrapped stream output function
    return ((initStreamState, 0), ServerStream loggedStreamNext)
    where
      mkLoggedStreamNext getNext (streamState, index) = do
        -- Get next chunk
        next <- getNext streamState
        case next of
          Nothing -> do
            -- Log end of stream
            serverStreamEndLogger rpcPath req (uuid, index)
            -- Return end of stream
            return Nothing
          Just (nextStreamState, replyMsg) -> do
            -- Log chunk
            serverStreamLogger rpcPath req (uuid, index) replyMsg
            -- The unwrapped stream output function returns the next stream state and the message to send
            -- We add log state
            return $ Just ((nextStreamState, index + 1), replyMsg)

      rpcPath = path rpc