hs-bindgen-1.0.0.0: src-internal/HsBindgen/Util/Tracer.hs
-- | Logging
--
-- Intended for unqualified import.
module HsBindgen.Util.Tracer (
-- * Tracer definition and main API
Tracer -- opaque
, traceWith
, WithCallStack(..)
, withCallStack
, simpleTracer
, nullTracer
-- * Data types and typeclasses useful for tracing
, PrettyForTrace (..)
, Level (..)
, SafeLevel (..)
, Source (..)
, TraceId (..)
, IsTrace (..)
, Verbosity (..)
, IsUserRequested (..)
, userRequestedIf
-- * Tracer configuration
, ShowCallStack (..)
, AnsiColor (..)
, Report
, OutputConfig (..)
, OutputHandle (..)
, OutputCustom (..)
, outputConfigTH
, CustomLogLevel (..)
, applyCustomLogLevel
, TracerConfig (..)
-- * Tracers
, withTracer
, TracerState(..)
, checkTracerState
, AnErrorHappened(..)
-- * Safe tracers
, withTracerSafe
, SafeTrace(..)
-- * Re-exports
, Contravariant(..)
-- * Test infrastructure
, withTracerUnsafe
) where
import Control.Tracer (Contravariant (..))
import Control.Tracer qualified as ContraTracer
import Data.Bool (bool)
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.Kind (Type)
import GHC.Generics as GHC
import GHC.Stack (CallStack, prettyCallStack)
import System.Console.ANSI (Color (..), ColorIntensity (Vivid),
ConsoleIntensity (BoldIntensity),
ConsoleLayer (Foreground),
SGR (SetColor, SetConsoleIntensity),
hSupportsANSIColor, setSGRCode)
import System.IO (Handle, hPutStr, stderr)
import Text.SimplePrettyPrint (Context, CtxDoc)
import Text.SimplePrettyPrint qualified as PP
import HsBindgen.Errors
import HsBindgen.Imports
{-------------------------------------------------------------------------------
Definition and main API
The definition of 'Tracer' is opaque.
-------------------------------------------------------------------------------}
newtype Tracer e = Wrap (ContraTracer.Tracer IO (WithCallStack e))
unwrap :: Tracer e -> ContraTracer.Tracer IO (WithCallStack e)
unwrap (Wrap tracer) = tracer
-- | We pair every trace message with a callstack for easier debugging
data WithCallStack e = WithCallStack {
callStack :: CallStack
, traceMsg :: e
}
deriving stock (Show, Functor)
instance PrettyForTrace e => PrettyForTrace (WithCallStack e) where
prettyForTrace = prettyForTrace . (.traceMsg)
instance IsTrace l e => IsTrace l (WithCallStack e) where
getDefaultLogLevel = getDefaultLogLevel . (.traceMsg)
getSource = getSource . (.traceMsg)
getTraceId = getTraceId . (.traceMsg)
instance Contravariant Tracer where
contramap f = Wrap . contramap (fmap f) . unwrap
traceWith :: MonadIO m => Tracer a -> WithCallStack a -> m ()
traceWith tracer =
liftIO
. ContraTracer.traceWith (unwrap tracer)
-- | Capture current callstack and pair with a value
--
-- Useful when collecting trace messages in pure code for later emission
-- via 'traceWith'.
withCallStack :: HasCallStack => a -> WithCallStack a
withCallStack = WithCallStack callStack
-- | Simple tracer that 'ContraTracer.emit's every message
simpleTracer :: (e -> IO ()) -> Tracer e
simpleTracer f = simpleWithCallStack (f . (.traceMsg))
-- | Generalization of 'simpleWithCallStack'
--
-- This is internal API.
simpleWithCallStack :: (WithCallStack e -> IO ()) -> Tracer e
simpleWithCallStack =
Wrap
. ContraTracer.Tracer
. ContraTracer.emit
-- | See 'ContraTracer.squelchUnless'
squelchUnless :: (e -> Bool) -> Tracer e -> Tracer e
squelchUnless p =
Wrap
. ContraTracer.squelchUnless (p . (.traceMsg))
. unwrap
nullTracer :: Tracer e
nullTracer = Wrap ContraTracer.nullTracer
{-------------------------------------------------------------------------------
Data types and type classes useful for tracing
-------------------------------------------------------------------------------}
-- | Convert values to textual representations used in traces.
class PrettyForTrace e where
prettyForTrace :: e -> CtxDoc
default prettyForTrace :: (Generic e, GPrettyForTrace (Rep e)) => e -> CtxDoc
prettyForTrace = gPrettyForTrace'
class GPrettyForTrace (r :: Type -> Type) where
gPrettyForTrace :: r x -> CtxDoc
instance GPrettyForTrace r => GPrettyForTrace (M1 tag meta r) where
gPrettyForTrace (M1 x) = gPrettyForTrace x
instance (GPrettyForTrace r1, GPrettyForTrace r2) => GPrettyForTrace (r1 :+: r2) where
gPrettyForTrace (L1 x) = gPrettyForTrace x
gPrettyForTrace (R1 x) = gPrettyForTrace x
instance PrettyForTrace e => GPrettyForTrace (K1 tag e) where
gPrettyForTrace (K1 x) = prettyForTrace x
gPrettyForTrace' :: (GHC.Generic e, GPrettyForTrace (GHC.Rep e)) => e -> CtxDoc
gPrettyForTrace' = gPrettyForTrace . GHC.from
-- | Log or verbosity level.
--
-- Careful, the derived 'Ord' instance is used when determining if a trace
-- should be emitted, or not.
data Level =
-- | Useful only for debugging hs-bindgen itself
Debug
-- | Regular progress, perhaps useful for debugging hs-bindgen config
--
-- E.g.: "Why why @foo@ not selected?"
| Info
-- | Normal but significant condition the user should be aware of
--
-- E.g.: "binding global variable may result in duplicate symbols"
| Notice
-- | We may produce incomplete bindings
| Warning
-- | We encountered something unexpected but may produce incomplete bindings
| Bug
-- | We are unable to produce /any/ bindings at all
| Error
deriving stock (Show, Eq, Ord, Bounded, Enum, Generic)
instance Default Level where
def = Notice
alignLevel :: Level -> String
alignLevel = \case
Debug -> "Debug "
Info -> "Info "
Notice -> "Notice "
Warning -> "Warning"
Bug -> "Bug "
Error -> "Error "
getColorForLevel :: Level -> Color
getColorForLevel = \case
Debug -> White
Info -> Green
Notice -> Yellow
Warning -> Yellow
Bug -> Red
Error -> Red
-- | Safe log or verbosity level to be used by the backend.
--
-- We intentionally limit 'SafeLevel' to debug and info messages. Notices,
-- warnings and errors should be handled in the frontend.
--
-- Traces with safe log levels can not abort the program.
data SafeLevel = SafeDebug | SafeInfo | SafeNotice
deriving stock (Show, Eq, Ord, Bounded)
fromSafeLevel :: SafeLevel -> Level
fromSafeLevel = \case
SafeDebug -> Debug
SafeInfo -> Info
SafeNotice -> Notice
toSafeLevel :: Level -> Maybe SafeLevel
toSafeLevel = \case
Debug -> Just SafeDebug
Info -> Just SafeInfo
Notice -> Just SafeNotice
_ -> Nothing
-- | Possible sources of traces. The 'Source' is shown by default in traces, and
-- so should be useful to users of @hs-bindgen@. At the moment, we only
-- distinguish between 'Libclang' and 'HsBindgen'.
data Source = Libclang | HsBindgen
deriving stock (Show, Eq)
alignSource :: Source -> String
alignSource = \case
Libclang -> "Libclang "
HsBindgen -> "HsBindgen"
newtype TraceId = TraceId { id :: String }
deriving stock (Show, Eq, Ord)
deriving (IsString, Semigroup, Monoid) via String
class PrettyForTrace e => IsTrace l e | e -> l where
-- | Get default (or suggested) log level of a trace.
getDefaultLogLevel :: e -> l
default getDefaultLogLevel :: (Generic e, GIsTrace l (Rep e)) => e -> l
getDefaultLogLevel = gGetDefaultLogLevel'
-- | Get source or context of trace.
getSource :: e -> Source
default getSource :: (Generic e, GIsTrace l (Rep e)) => e -> Source
getSource = gGetSource'
-- | The trace identifier does not necessarily have to be unique. For example,
-- an non-unique identifier may be used to collect multiple traces in a single
-- category.
--
-- The trace identifier usually starts with a letter and contains only letters
-- and dashes (e.g., @my-trace-id@). A good default is the constructor name in
-- kebab-case, leaving out potential "-trace" or "-msg" suffixes.
getTraceId :: e -> TraceId
default getTraceId :: (Generic e, GIsTrace l (Rep e)) => e -> TraceId
getTraceId = gGetTraceId'
class GIsTrace l (r :: Type -> Type) | r -> l where
gGetDefaultLogLevel :: r x -> l
gGetSource :: r x -> Source
gGetTraceId :: r x -> TraceId
instance GIsTrace l r => GIsTrace l (M1 tag meta r) where
gGetDefaultLogLevel (M1 x) = gGetDefaultLogLevel x
gGetSource (M1 x) = gGetSource x
gGetTraceId (M1 x) = gGetTraceId x
instance (GIsTrace l r1, GIsTrace l r2) => GIsTrace l (r1 :+: r2) where
gGetDefaultLogLevel (L1 x) = gGetDefaultLogLevel x
gGetDefaultLogLevel (R1 x) = gGetDefaultLogLevel x
gGetSource (L1 x) = gGetSource x
gGetSource (R1 x) = gGetSource x
gGetTraceId (L1 x) = gGetTraceId x
gGetTraceId (R1 x) = gGetTraceId x
instance IsTrace l a => GIsTrace l (K1 tag a) where
gGetDefaultLogLevel (K1 x) = getDefaultLogLevel x
gGetSource (K1 x) = getSource x
gGetTraceId (K1 x) = getTraceId x
gGetDefaultLogLevel' :: (GHC.Generic e, GIsTrace l (GHC.Rep e)) => e -> l
gGetDefaultLogLevel' = gGetDefaultLogLevel . GHC.from
gGetSource' :: (GHC.Generic e, GIsTrace l (GHC.Rep e)) => e -> Source
gGetSource' = gGetSource . GHC.from
gGetTraceId' :: (GHC.Generic e, GIsTrace l (GHC.Rep e)) => e -> TraceId
gGetTraceId' = gGetTraceId . GHC.from
newtype Verbosity = Verbosity { level :: Level }
deriving stock (Show, Eq)
deriving Default via Level
-- | User requested status
--
-- The log level of some trace messages depend on if the user explicitly
-- requested something or not.
data IsUserRequested = UserRequested | NotUserRequested
deriving stock (Show, Eq)
userRequestedIf :: Bool -> IsUserRequested
userRequestedIf = bool NotUserRequested UserRequested
{-------------------------------------------------------------------------------
Tracer configuration
-------------------------------------------------------------------------------}
data AnsiColor = EnableAnsiColor | DisableAnsiColor
deriving stock (Show, Eq)
data ShowCallStack = EnableCallStack | DisableCallStack
deriving stock (Show, Eq)
-- | We provide both, the trace itself and the formatted message. For example, a
-- report function may want to store the trace only, and ignore the formatted
-- message.
--
-- Further, a report function may need to know about the log level. For example,
-- with Template Haskell, errors and warnings are treated differently compared
-- to debug and info messages. NOTE: Traces do have default log levels, but we
-- need the custom log level. This is sub-optimal. We could use newtype wrappers
-- to directly change the log level, and not provide the custom log level
-- separately here.
--
-- The report function has access to the typed trace @a@, and the formatted
-- trace. The formatted trace also possibly contains the call stack, or other
-- information.
type Report e = Level -> e -> String -> IO ()
data OutputConfig e =
OutputConfigHandle OutputHandle
| OutputConfigCustom (OutputCustom e)
data OutputHandle = OutputHandle{
handle :: Handle
-- | ANSI color support
--
-- 'Nothing': Automatically determine ANSI color support by examining the
-- 'Handle'; see 'getAnsiColor'.
, ansiColor :: Maybe AnsiColor
}
data OutputCustom e = OutputCustom{
report :: Report e
, ansiColor :: AnsiColor
}
instance Contravariant OutputConfig where
contramap f = \case
OutputConfigHandle handle -> OutputConfigHandle handle
OutputConfigCustom custom -> OutputConfigCustom (contramap f custom)
instance Contravariant OutputCustom where
contramap f outputCustom = outputCustom{
report = \level -> outputCustom.report level . f
}
-- | The default tracer configuration
--
-- - writes to 'stderr', and
-- - uses ANSI colors, if available.
instance Default (OutputConfig e) where
def = OutputConfigHandle def
instance Default OutputHandle where
def = OutputHandle{
handle = stderr
, ansiColor = Nothing
}
-- | Output configuration suitable for compile-time code generation with
-- Template Haskell.
--
-- Report all traces to @stderr@, without ANSI colours (GHC output is usually
-- captured by the build tool).
--
-- NOTE: We cannot use GHC's own diagnostics (@reportWarning@/@reportError@),
-- because the pipeline runs in @IO@, not in @Q@; @runQ@ would then pick
-- @instance Quasi IO@, which labels /every/ message a Template Haskell error.
outputConfigTH :: OutputConfig e
outputConfigTH = OutputConfigHandle OutputHandle{
handle = stderr
, ansiColor = Just DisableAnsiColor
}
-- | Sometimes, we want to change log levels. For example, we want to suppress
-- specific traces in tests.
--
-- The custom log level function takes a trace and returns a function
-- customizing the log level.
newtype CustomLogLevel l e = CustomLogLevel (e -> l -> l)
applyCustomLogLevel :: CustomLogLevel l e -> e -> l -> l
applyCustomLogLevel (CustomLogLevel f) = f
-- | First apply the left custom log level, then the right one.
instance Semigroup (CustomLogLevel l e) where
(CustomLogLevel left) <> (CustomLogLevel right) =
CustomLogLevel $ \trc -> right trc . left trc
instance Monoid (CustomLogLevel l e) where
mempty = CustomLogLevel $ const id
instance Contravariant (CustomLogLevel l) where
contramap f (CustomLogLevel g) = CustomLogLevel $ g . f
-- | Configuration of tracer.
data TracerConfig l e = TracerConfig{
verbosity :: Verbosity
, outputConfig :: OutputConfig e
, customLogLevel :: CustomLogLevel l e
, showCallStack :: ShowCallStack
}
deriving (Generic)
instance Contravariant (TracerConfig l) where
contramap f config = config {
outputConfig = contramap f config.outputConfig
, customLogLevel = contramap f config.customLogLevel
}
instance Default (TracerConfig l e) where
def = TracerConfig{
verbosity = def
, outputConfig = def
, customLogLevel = mempty
, showCallStack = DisableCallStack
}
{-------------------------------------------------------------------------------
Tracers
-------------------------------------------------------------------------------}
-- | Run an action with a tracer.
--
-- The default tracer configuration
--
-- - writes to @stdout@, and
-- - uses ANSI colors, if available.
--
-- Return 'Nothing' if an 'Error' trace was emitted.
withTracer :: forall m e a. (MonadIO m , IsTrace Level e)
=> TracerConfig Level e
-> (Tracer e -> m a)
-> m (Either AnErrorHappened a)
withTracer tracerConf action = withTracerUnsafe tracerConf action'
where
action' :: Tracer e -> IORef TracerState -> m (Either AnErrorHappened a)
action' tracer ref = do
r <- action tracer
s <- liftIO $ readIORef ref
case s of
TracerState Error -> pure $ Left AnErrorHappened
_ -> pure $ Right r
-- | Internal. The tracer stores the maximum log level of emitted traces and all
-- emitted error traces.
data TracerState = TracerState {
tracerMaxLevel :: Level
}
defTracerState :: TracerState
defTracerState = TracerState Debug
-- | Run an action with a tracer.
--
-- The caller is responsible for checking for errors in the tracer state.
--
-- Used in tests.
withTracerUnsafe :: forall m e a. (MonadIO m, IsTrace Level e)
=> TracerConfig Level e
-> (Tracer e -> IORef TracerState -> m a)
-> m a
withTracerUnsafe config action = do
(report, ansiColor) <- getOutputConfig
fmap fst $ withIORef defTracerState $ \ref ->
action (mkTracer
config.customLogLevel
ref
config.verbosity
ansiColor
config.showCallStack
report)
ref
where
getOutputConfig :: m (Report e, AnsiColor)
getOutputConfig = case config.outputConfig of
OutputConfigHandle outputHandle -> do
ansiColor <- case outputHandle.ansiColor of
Nothing -> getAnsiColor outputHandle.handle
Just x -> pure x
let report _lvl _trace = liftIO . hPutStr outputHandle.handle
pure (report, ansiColor)
OutputConfigCustom outputCustom ->
pure (outputCustom.report, outputCustom.ansiColor)
{-------------------------------------------------------------------------------
Trace error
-------------------------------------------------------------------------------}
-- | Evidence that an error has happened.
--
-- We do not store/report the actual errors, because we have emitted them
-- previously.
data AnErrorHappened = AnErrorHappened
deriving stock (Show)
instance PrettyForTrace AnErrorHappened where
prettyForTrace AnErrorHappened = "An error happened (see above)"
{-------------------------------------------------------------------------------
Safe tracer
-------------------------------------------------------------------------------}
-- | Run an action with a safe tracer using a custom output configuration.
--
-- Always returns a result (if the action does not panic).
--
-- See 'SafeLevel'.
withTracerSafe :: forall m e a. (MonadIO m, IsTrace SafeLevel e)
=> TracerConfig SafeLevel e
-> (Tracer e -> m a)
-> m a
withTracerSafe config action =
withTracerUnsafe tracerConf' (\t _ -> action' t)
where
action' :: Tracer (SafeTrace e) -> m a
action' = action . contramap SafeTrace
toCustomLogLevelUnsafe :: CustomLogLevel SafeLevel c -> CustomLogLevel Level c
toCustomLogLevelUnsafe (CustomLogLevel f) = CustomLogLevel $ \trc lvl ->
-- NOTE: Only customize safe levels, and do not change unsafe levels.
-- However, the latter case should never happen!
case toSafeLevel lvl of
Just safeLvl -> fromSafeLevel $ f trc safeLvl
Nothing -> lvl
customLogLevel :: CustomLogLevel Level (SafeTrace e)
customLogLevel =
toCustomLogLevelUnsafe $
contramap (.trace) config.customLogLevel
tracerConf' :: TracerConfig Level (SafeTrace e)
tracerConf' = config {
outputConfig = contramap (.trace) config.outputConfig
, customLogLevel = customLogLevel
}
newtype SafeTrace e = SafeTrace { trace :: e }
deriving (Show, Eq, Generic)
instance PrettyForTrace e => PrettyForTrace (SafeTrace e) where
prettyForTrace = prettyForTrace . (.trace)
instance IsTrace SafeLevel e => IsTrace Level (SafeTrace e) where
getDefaultLogLevel = fromSafeLevel . getDefaultLogLevel . (.trace)
getSource = getSource . (.trace)
getTraceId = getTraceId . (.trace)
{-------------------------------------------------------------------------------
Internal helpers
-------------------------------------------------------------------------------}
-- | Create a tracer emitting traces to a provided function @report@.
--
-- The traces provide additional information about
-- - the log level, and
-- - the source.
mkTracer :: forall e. (IsTrace Level e)
=> CustomLogLevel Level e
-> IORef TracerState
-> Verbosity
-> AnsiColor
-> ShowCallStack
-> Report e
-> Tracer e
mkTracer
customLogLevel
tracerStateRef
verbosity
ansiColor
showCallStack
report =
squelchUnless isLogLevelHighEnough $ simpleWithCallStack $ traceAction
where
isLogLevelHighEnough :: e -> Bool
isLogLevelHighEnough trace = getLogLevel trace >= verbosity.level
traceAction :: WithCallStack e -> IO ()
traceAction msg = do
liftIO $ modifyIORef' tracerStateRef $ updateTracerState level
msgTrace <- formatTrace ansiColor level msg.traceMsg
let msgStack = prettyCallStack msg.callStack
components = msgTrace : [ msgStack | showCallStack == EnableCallStack ]
report level msg.traceMsg $ unlines components
where
level :: Level
level = getLogLevel msg.traceMsg
updateTracerState :: Level -> TracerState -> TracerState
updateTracerState level (TracerState maxLevel) =
TracerState $ max level maxLevel
getLogLevel :: e -> Level
getLogLevel x = applyCustomLogLevel customLogLevel x (getDefaultLogLevel x)
-- | Render a string in bold and a specified color.
--
-- Careful, the applied ANSI code suffix also resets all other activated formatting.
withColor :: AnsiColor -> Level -> String -> String
withColor DisableAnsiColor _ = id
withColor EnableAnsiColor level = withColor' (getColorForLevel level)
where
withColor' :: Color -> String -> String
withColor' color x = setColor <> x <> resetColor
where
setColor :: String
setColor = setSGRCode [
SetColor Foreground Vivid color
, SetConsoleIntensity BoldIntensity
]
resetColor :: String
resetColor = setSGRCode []
-- | Check a TracerState IORef for errors.
--
-- This is useful for checking for errors after forcing lazy computations that
-- trace to a tracer whose state is captured in the IORef.
checkTracerState :: MonadIO m => IORef TracerState -> m (Maybe AnErrorHappened)
checkTracerState ref = do
tracerState <- liftIO $ readIORef ref
pure $ case tracerState of
TracerState Error -> Just AnErrorHappened
_ -> Nothing
withIORef :: MonadIO m => b -> (IORef b -> m a) -> m (a, b)
withIORef initialValue action = do
ref <- liftIO $ newIORef initialValue
actionResult <- action ref
refResult <- liftIO $ readIORef ref
pure (actionResult, refResult)
getAnsiColor :: MonadIO m => Handle -> m AnsiColor
getAnsiColor handle = do
supportsAnsiColor <- liftIO $ hSupportsANSIColor handle
pure $ if supportsAnsiColor then EnableAnsiColor else DisableAnsiColor
-- Format a trace message.
type Format m e = AnsiColor -> Level -> e -> m String
-- Log format:
-- [LEVEL] [SOURCE] Message.
-- Indent subsequent lines.
-- OPTION CALL STACK.
formatTrace :: (MonadIO m, IsTrace Level e) => Format m e
formatTrace ansiColor level trace =
pure $
formatLine $
appendBugNote $
PP.renderCtxDoc context $
prettyForTrace trace
where
context :: Context
context = PP.mkContext 120
source :: Source
source = getSource trace
traceId :: TraceId
traceId = getTraceId trace
prependLevel :: String -> String
prependLevel x =
withColor ansiColor level ("[" <> alignLevel level <> "]") <> " " <> x
prependSource :: String -> String
prependSource x =
withColor ansiColor level ("[" <> alignSource source <> "]") <> " " <> x
prependTraceId :: String -> String
prependTraceId x =
withColor ansiColor level ("[" <> traceId.id <> "]") <> " " <> x
formatLine :: String -> String
formatLine = prependLevel . prependSource . prependTraceId
appendBugNote :: String -> String
appendBugNote x = case level of
Bug -> x ++ "\n" ++ pleaseReport
_otherLvl -> x