packages feed

eventlog-live-0.3.0.0: src/GHC/Eventlog/Live/Logger.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : GHC.Eventlog.Live..Logger
Description : Logging functions.
Stability   : experimental
Portability : portable
-}
module GHC.Eventlog.Live.Logger (
  logError,
  logWarning,
  logInfo,
  logDebug,
) where

import Control.Exception (bracket_)
import Control.Monad.IO.Class (MonadIO (..))
import Data.List qualified as L
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as TIO
import GHC.Eventlog.Live.Verbosity (Verbosity, showVerbosity, verbosityDebug, verbosityError, verbosityInfo, verbosityWarning)
import GHC.Stack (CallStack, HasCallStack, SrcLoc (..), callStack, getCallStack)
import System.Console.ANSI (Color (..), ColorIntensity (..), ConsoleLayer (..), SGR (..), hNowSupportsANSI, hSetSGR)
import System.IO qualified as IO

{- |
Log messages to given handle.
Only prints a message if its verbosity level is above the verbosity threshold.
-}
logMessage :: (MonadIO m) => IO.Handle -> CallStack -> Verbosity -> Verbosity -> Text -> m ()
logMessage handle theCallStack verbosityLevel verbosityThreshold msg
  | verbosityLevel >= verbosityThreshold = liftIO $ do
      withVerbosityColor verbosityLevel handle
        . flip TIO.hPutStrLn
        . formatMessage verbosityLevel verbosityThreshold theCallStack
        $ msg
      IO.hFlush handle
  | otherwise = pure ()

{- |
Internal helper.
Format the `CallStack`.
-}
formatCallStack :: CallStack -> Text
formatCallStack theCallStack =
  maybe T.empty (formatSrcLoc . snd . fst) (L.uncons (getCallStack theCallStack))
 where
  formatSrcLoc :: SrcLoc -> Text
  formatSrcLoc srcLoc =
    mconcat [T.pack srcLoc.srcLocFile, ":", T.pack (show srcLoc.srcLocStartLine), ":", T.pack (show srcLoc.srcLocStartCol)]

{- |
Internal helper.
Format the message appropriately for the given verbosity level and threshold.
-}
formatMessage :: Verbosity -> Verbosity -> CallStack -> Text -> Text
formatMessage verbosityLevel verbosityThreshold theCallStack msg
  | verbosityLevel == verbosityInfo && verbosityThreshold /= verbosityDebug = msg
  | otherwise = mconcat [showVerbosity verbosityLevel, " (", formatCallStack theCallStack, "): ", msg]

{- |
Internal helper.
Use a handle with the color set appropriately for the given verbosity level.
-}
withVerbosityColor :: Verbosity -> IO.Handle -> (IO.Handle -> IO a) -> IO a
withVerbosityColor verbosity handle action = do
  supportsANSI <- hNowSupportsANSI handle
  if not supportsANSI
    then
      action handle
    else case verbosityColor verbosity of
      Nothing ->
        action handle
      Just color -> do
        let setVerbosityColor = hSetSGR handle [SetColor Foreground Vivid color]
        let setDefaultColor = hSetSGR handle [SetDefaultColor Foreground]
        bracket_ setVerbosityColor setDefaultColor $ action handle

{- |
Internal helper.
Determine the ANSI color associated with a particular verbosity level.
-}
verbosityColor :: Verbosity -> Maybe Color
verbosityColor verbosity
  | verbosity == verbosityError = Just Red
  | verbosity == verbosityWarning = Just Yellow
  | verbosity == verbosityDebug = Just Blue
  | otherwise = Nothing

{- |
Log errors to `IO.stderr`.
-}
logError :: (HasCallStack, MonadIO m) => Verbosity -> Text -> m ()
logError = logMessage IO.stderr callStack verbosityError

{- |
Log warnings to `IO.stderr`.
-}
logWarning :: (HasCallStack, MonadIO m) => Verbosity -> Text -> m ()
logWarning = logMessage IO.stderr callStack verbosityWarning

{- |
Log info messages to `IO.stderr`.
-}
logInfo :: (HasCallStack, MonadIO m) => Verbosity -> Text -> m ()
logInfo = logMessage IO.stdout callStack verbosityInfo

{- |
Log debug messages to `IO.stderr`.
-}
logDebug :: (HasCallStack, MonadIO m) => Verbosity -> Text -> m ()
logDebug = logMessage IO.stderr callStack verbosityDebug