packages feed

polysemy-http-0.1.0.0: lib/Polysemy/Http/Log.hs

module Polysemy.Http.Log where

import Colog.Core (LogAction(LogAction), Severity, cmapM)
import qualified Colog.Core as Severity (Severity(Error, Info, Debug, Warning))
import qualified Colog.Polysemy as Colog (Log, log)
import Colog.Polysemy.Effect (runLogAction)
import qualified Data.ByteString.Char8 as BS8
import GHC.Stack (SrcLoc(SrcLoc), popCallStack, srcLocModule, srcLocStartLine)
import System.Console.ANSI (Color (..), ColorIntensity (Vivid), ConsoleLayer (Foreground), SGR (..), setSGRCode)

import Polysemy.Http.Data.Log (Log(..))

data Message =
  Message {
    msgSeverity :: !Severity,
    msgStack :: !CallStack,
    msgText :: !Text
  }

showSeverity :: Severity -> Text
showSeverity = \case
  Severity.Debug -> color Green "[Debug]   "
  Severity.Info -> color Blue "[Info]    "
  Severity.Warning -> color Yellow "[Warning] "
  Severity.Error -> color Red "[Error]   "
 where
   color :: Color -> Text -> Text
   color c txt =
     toText (setSGRCode [SetColor Foreground Vivid c]) <>
     txt <>
     toText (setSGRCode [Reset])

showSourceLoc :: CallStack -> Text
showSourceLoc cs =
  square showCallStack
  where
    showCallStack :: Text
    showCallStack = case getCallStack cs of
      [] -> "<unknown loc>"
      [(name, loc)] -> showLoc name loc
      (_, loc) : (callerName, _) : _ -> showLoc callerName loc
    square t = "[" <> t <> "] "
    showLoc :: String -> SrcLoc -> Text
    showLoc name SrcLoc{..} =
      toText srcLocModule <> "." <> toText name <> "#" <> show srcLocStartLine

fmtRichMessageDefault :: Message -> IO Text
fmtRichMessageDefault =
  pure . formatRichMessage
  where
    formatRichMessage :: Message -> Text
    formatRichMessage Message{..} =
        showSeverity msgSeverity
     <> showSourceLoc msgStack
     <> msgText

logByteStringStdout :: LogAction IO ByteString
logByteStringStdout = LogAction BS8.putStrLn

richMessageAction :: LogAction IO Message
richMessageAction =
    cmapM (fmap encodeUtf8 . fmtRichMessageDefault) logByteStringStdout
{-# INLINE richMessageAction #-}

log ::
  HasCallStack =>
  Member (Colog.Log Message) r =>
  Severity ->
  Text ->
  Sem r ()
log severity text =
  Colog.log (Message severity stack text)
  where
    stack =
      popCallStack (popCallStack callStack)

-- |No-op interpreter for 'Log'
interpretLogNull ::
  InterpreterFor Log r
interpretLogNull =
  interpret \case
    Debug _ -> unit
    Info _ -> unit
    Warn _ -> unit
    Error _ -> unit
    ErrorPlus _ _ -> unit
{-# INLINE interpretLogNull #-}

runCologStdout ::
  Member (Embed IO) r =>
  InterpreterFor (Colog.Log Message) r
runCologStdout =
  runLogAction @IO richMessageAction
{-# INLINE runCologStdout #-}

toColog ::
  HasCallStack =>
  Member (Colog.Log Message) r =>
  Sem (Log : r) a ->
  Sem r a
toColog =
  interpret \case
    Debug msg ->
      log Severity.Debug msg
    Info msg ->
      log Severity.Info msg
    Warn msg ->
      log Severity.Warning msg
    Error msg ->
      log Severity.Error msg
    ErrorPlus msg detailed ->
      log Severity.Error msg *> traverse_ (log Severity.Debug) detailed
{-# INLINE toColog #-}

-- |Default interpreter for 'Log' that uses 'Colog.Log' to print to stdout
interpretLogStdout ::
  Member (Embed IO) r =>
  Sem (Log : r) a ->
  Sem r a
interpretLogStdout =
  runCologStdout . toColog . raiseUnder
{-# INLINE interpretLogStdout #-}