{- | Here are debug tracing/logging helpers built on Debug.Trace, extracted from the hledger project. Features: - they can be built in to your program permanently, and activated by a --debug [LEVEL] option - they can optionally log to a file instead of stderr (for TUI apps) - they can be used in IO, pure, or startup code - values are printed with a label, and pretty-printed (using pretty-simple) - ANSI colour is used when appropriate. Insert these @dbg*@ helpers at points of interest in your code, either temporarily while debugging, or permanently in production code, and activate them with @--debug [1-9]@ on the command line (@--debug@ with no value means level 1). For example, this expression: > dbg4 "foo" foo will pretty-print foo with a "foo:" label when it is evaluated, but only if --debug's value is 4 or greater. In other words: use dbg1 for the most useful debug output, dbg9 for the most specialised/verbose. They are intended to be easy to use and to find in your code, with a consistent naming scheme: > dbgMsg STR VAL -- trace/log a string in pure code > dbgMsgIO STR -- trace/log a string in IO > > dbg STR VAL -- trace/log a showable value in pure code > dbgIO STR VAL -- trace/log a showable value in IO > > dbgWith SHOWFN VAL -- trace/log any value Or if you prefer you can ignore the numbered variants and write an extra argument: > dbgMsg LEVEL STR VAL > dbgMsgIO LEVEL STR > > dbg LEVEL STR VAL > dbgIO LEVEL STR VAL > > dbgWith LEVEL SHOWFN VAL Haskell values will be pretty-printed by default, using pretty-simple. ANSI color will also be used if appropriate, respecting output capabilities, @NO_COLOR@, and/or a @--color [YNA]@ (or @--colour@) command line option. These helpers normally print output on stderr, but can automatically log to a file instead, which can be useful for TUI apps which are redrawing the screen. To enable this logging mode, use @withProgName@ to add a ".log" suffix to the program name: > main = withProgName "PROGRAM.log" $ do ... Now all dbg calls will log to @PROGRAM.log@ in the current directory. Logging, and reading the command line\/program name\/output context use unsafePerformIO, so that these can be used anywhere, including early in your program before command line parsing is complete. As a consequence, if you are testing in GHCI and want to change the debug level, you'll need to reload this module. The @dbg@ function name clashes with the one in Text.Megaparsec.Debug, unfortunately; sorry about that. If you are also using that, use qualified imports, or our @dbg_@ alias, to avoid the clash. The meaning of debug levels is up to you. Eg hledger uses them as follows: @ Debug level: What to show: ------------ --------------------------------------------------------- 0 normal program output only 1 useful warnings, most common troubleshooting info 2 common troubleshooting info, more detail 3 report options selection 4 report generation 5 report generation, more detail 6 input file reading 7 input file reading, more detail 8 command line parsing 9 any other rarely needed / more in-depth info @ It's not yet possible to select debug output by topic; that would be useful. -} -- Disabled until 0.1.2.0 is released with windows support: -- This module also exports Debug.Trace and the breakpoint package's Debug.Breakpoint. -- more: -- http://hackage.haskell.org/packages/archive/TraceUtils/0.1.0.2/doc/html/Debug-TraceUtils.html -- http://hackage.haskell.org/packages/archive/trace-call/0.1/doc/html/Debug-TraceCall.html -- http://hackage.haskell.org/packages/archive/htrace/0.1/doc/html/Debug-HTrace.html -- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html -- https://hackage.haskell.org/package/debug -- internal helpers, currently not exported: -- * Tracing to stderr -- These print to stderr. -- This output will be interleaved with the program's normal output, -- which can be helpful for understanding code execution. -- -- ,traceWith -- ,traceAt -- ,traceAtWith -- ,ptrace -- ,ptraceAt -- ,ptraceAtIO -- * Logging to a log file -- These append to a PROGRAM.log file in the current directory. -- PROGRAM is normally the name of the executable, but it can change -- eg when running in GHCI. So when using these, you should call -- @withProgName@ to ensure a stable program name. -- Eg: @main = withProgName "PROGRAM" $ do ...@. -- -- ,log' -- ,logAt -- ,logIO -- ,logAtIO -- ,logWith -- ,logAtWith -- ,plogAt -- ,plogAtIO {-# LANGUAGE CPP #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} module Hledger.Utils.Debug ( -- * Debug level debugLevel -- * Trace/log a string ,dbgMsg ,dbg0Msg ,dbg1Msg ,dbg2Msg ,dbg3Msg ,dbg4Msg ,dbg5Msg ,dbg6Msg ,dbg7Msg ,dbg8Msg ,dbg9Msg -- * Trace/log a string in IO ,dbgMsgIO ,dbg0MsgIO ,dbg1MsgIO ,dbg2MsgIO ,dbg3MsgIO ,dbg4MsgIO ,dbg5MsgIO ,dbg6MsgIO ,dbg7MsgIO ,dbg8MsgIO ,dbg9MsgIO -- * Trace/log a value ,dbg ,dbg_ ,dbg0 ,dbg1 ,dbg2 ,dbg3 ,dbg4 ,dbg5 ,dbg6 ,dbg7 ,dbg8 ,dbg9 -- * Trace/log a value in IO ,dbgIO ,dbg0IO ,dbg1IO ,dbg2IO ,dbg3IO ,dbg4IO ,dbg5IO ,dbg6IO ,dbg7IO ,dbg8IO ,dbg9IO -- * Trace/log a value with a show function ,dbgWith ,dbg0With ,dbg1With ,dbg2With ,dbg3With ,dbg4With ,dbg5With ,dbg6With ,dbg7With ,dbg8With ,dbg9With -- * Utilities ,lbl_ ,progName -- * ghc-debug helpers ,ghcDebugSupportedInLib ,GhcDebugMode(..) ,ghcDebugMode ,withGhcDebug' ,ghcDebugPause' -- * Re-exports: Debug.Trace -- ,module Debug.Breakpoint ,module Debug.Trace ) where import Control.DeepSeq (force) import Control.Exception (evaluate) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.List hiding (uncons) import Debug.Trace (trace, traceIO, traceShowId) #ifdef GHCDEBUG import GHC.Debug.Stub (pause, withGhcDebug) #endif import Safe (readDef) import System.Environment (getProgName) -- import System.Exit (exitFailure) import System.IO.Unsafe (unsafePerformIO) import Hledger.Utils.IO (progArgs, pshow, pshow') -- | The program name as returned by @getProgName@. -- It's best to set this explicitly at program startup with @withProgName@, -- otherwise when running in GHCI (eg) it will change to "". -- Setting it with a ".log" suffix causes some functions below -- to log instead of trace. {-# NOINLINE modifiedProgName #-} modifiedProgName :: String modifiedProgName = unsafePerformIO getProgName -- | The progam name, with any ".log" suffix removed. progName :: String progName = if ".log" `isSuffixOf` modifiedProgName then reverse $ drop 4 $ reverse modifiedProgName else modifiedProgName -- | The program's debug output verbosity, from 0 to 9. -- The default is 0 meaning no debug output. -- This can be overridden by running the program with a --debug [1-9] command line option; -- a --debug flag with no value means 1. -- Uses unsafePerformIO to read the command line. -- When running in GHCI, changing this requires reloading this module. debugLevel :: Int debugLevel = case dropWhile (/="--debug") progArgs of ["--debug"] -> 1 "--debug":n:_ -> readDef 1 n _ -> case take 1 $ filter ("--debug" `isPrefixOf`) progArgs of ['-':'-':'d':'e':'b':'u':'g':'=':v] -> readDef 1 v _ -> 0 -- | Trace (print to stderr) a string if the program debug level is at -- or above the specified level. At level 0, always prints. Otherwise, -- uses unsafePerformIO. traceAt :: Int -> String -> a -> a traceAt level | level > 0 && debugLevel < level = const id | otherwise = trace -- | Like traceAt, but sequences properly in IO. traceAtIO :: (MonadIO m) => Int -> String -> m () traceAtIO level msg = if level > 0 && debugLevel < level then return () else liftIO $ traceIO msg -- -- | Trace a value with the given show function before returning it. -- traceWith :: (a -> String) -> a -> a -- traceWith f a = trace (f a) a -- | Trace (print to stderr) a showable value using a custom show function, -- if the program debug level is at or above the specified level. -- At level 0, always prints. Otherwise, uses unsafePerformIO. traceAtWith :: Int -> (a -> String) -> a -> a traceAtWith level f a = traceAt level (f a) a -- -- | Pretty-trace a showable value before returning it. -- -- Like Debug.Trace.traceShowId, but pretty-printing and easier to type. -- ptrace :: Show a => a -> a -- ptrace = traceWith pshow -- | Pretty-print a label and a showable value to the console -- if the program debug level is at or above the specified level. -- At level 0, always prints. Otherwise, uses unsafePerformIO. ptraceAt :: Show a => Int -> String -> a -> a ptraceAt level | level > 0 && debugLevel < level = const id | otherwise = \lbl a -> trace (labelledPretty True lbl a) a -- Pretty-print a showable value with a label, with or without allowing ANSI color. labelledPretty :: Show a => Bool -> String -> a -> String labelledPretty allowcolour lbl a = lbl ++ ":" ++ nlorspace ++ intercalate "\n" ls' where ls = lines $ (if allowcolour then pshow else pshow') a nlorspace | length ls > 1 = "\n" | otherwise = replicate (max 1 $ 11 - length lbl) ' ' ls' | length ls > 1 = map (' ':) ls | otherwise = ls -- | Like ptraceAt, but sequences properly in IO. ptraceAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m () ptraceAtIO level label a = if level > 0 && debugLevel < level then return () else liftIO $ traceIO (labelledPretty True label a) -- | The debug log file: PROGNAME.log in the current directory. -- See modifiedProgName. debugLogFile :: FilePath debugLogFile = progName ++ ".log" -- | Log a string to the debug log before returning the second argument. -- Uses unsafePerformIO. log' :: String -> a -> a log' s x = unsafePerformIO $ do evaluate (force s) -- to complete any previous logging before we attempt more appendFile debugLogFile (s ++ "\n") return x -- | Log a string to the debug log before returning the second argument, -- if the program debug level is at or above the specified level. -- At level 0, always logs. Otherwise, uses unsafePerformIO. logAt :: Int -> String -> a -> a logAt level str | level > 0 && debugLevel < level = id | otherwise = log' str -- | Like log' but sequences properly in IO. logIO :: MonadIO m => String -> m () logIO s = do liftIO $ evaluate (force s) -- to complete any previous logging before we attempt more liftIO $ appendFile debugLogFile (s ++ "\n") -- | Like logAt, but convenient to use in IO. logAtIO :: (MonadIO m) => Int -> String -> m () logAtIO level str | level > 0 && debugLevel < level = return () | otherwise = logIO str -- -- | Log a value to the debug log with the given show function before returning it. -- logWith :: (a -> String) -> a -> a -- logWith f a = log' (f a) a -- | Log a string to the debug log before returning the second argument, -- if the program debug level is at or above the specified level. -- At level 0, always logs. Otherwise, uses unsafePerformIO. logAtWith :: Int -> (a -> String) -> a -> a logAtWith level f a = logAt level (f a) a -- | Pretty-log a label and showable value to the debug log, -- if the program debug level is at or above the specified level. -- At level 0, always prints. Otherwise, uses unsafePerformIO. plogAt :: (Show a) => Int -> String -> a -> a plogAt level | level > 0 && debugLevel < level = const id | otherwise = \lbl a -> log' (labelledPretty False lbl a) a -- | Like ptraceAt, but sequences properly in IO. plogAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m () plogAtIO level label a = if level > 0 && debugLevel < level then return () else logIO (labelledPretty False label a) -- | Should dbg* log to a file instead of tracing to stderr ? -- True if the (internal) program name ends with ".log". shouldLog :: Bool shouldLog = ".log" `isSuffixOf` modifiedProgName -- | Trace or log a string if the program debug level is at or above the specified level, -- then return the second argument. dbgMsg :: Int -> String -> a -> a dbgMsg = if shouldLog then logAt else traceAt dbg0Msg :: String -> a -> a dbg0Msg = dbgMsg 0 dbg1Msg :: String -> a -> a dbg1Msg = dbgMsg 1 dbg2Msg :: String -> a -> a dbg2Msg = dbgMsg 2 dbg3Msg :: String -> a -> a dbg3Msg = dbgMsg 3 dbg4Msg :: String -> a -> a dbg4Msg = dbgMsg 4 dbg5Msg :: String -> a -> a dbg5Msg = dbgMsg 5 dbg6Msg :: String -> a -> a dbg6Msg = dbgMsg 6 dbg7Msg :: String -> a -> a dbg7Msg = dbgMsg 7 dbg8Msg :: String -> a -> a dbg8Msg = dbgMsg 8 dbg9Msg :: String -> a -> a dbg9Msg = dbgMsg 9 -- | Like dbgMsg, but sequences properly in IO. dbgMsgIO :: (MonadIO m) => Int -> String -> m () dbgMsgIO = if shouldLog then logAtIO else traceAtIO dbg0MsgIO :: (MonadIO m) => String -> m () dbg0MsgIO = dbgMsgIO 0 dbg1MsgIO :: (MonadIO m) => String -> m () dbg1MsgIO = dbgMsgIO 1 dbg2MsgIO :: (MonadIO m) => String -> m () dbg2MsgIO = dbgMsgIO 2 dbg3MsgIO :: (MonadIO m) => String -> m () dbg3MsgIO = dbgMsgIO 3 dbg4MsgIO :: (MonadIO m) => String -> m () dbg4MsgIO = dbgMsgIO 4 dbg5MsgIO :: (MonadIO m) => String -> m () dbg5MsgIO = dbgMsgIO 5 dbg6MsgIO :: (MonadIO m) => String -> m () dbg6MsgIO = dbgMsgIO 6 dbg7MsgIO :: (MonadIO m) => String -> m () dbg7MsgIO = dbgMsgIO 7 dbg8MsgIO :: (MonadIO m) => String -> m () dbg8MsgIO = dbgMsgIO 8 dbg9MsgIO :: (MonadIO m) => String -> m () dbg9MsgIO = dbgMsgIO 9 -- | Trace or log a label and showable value, pretty-printed, -- if the program debug level is at or above the specified level; -- then return the value. dbg :: (Show a) => Int -> String -> a -> a dbg = if shouldLog then plogAt else ptraceAt -- | Alias for dbg, can be used to avoid namespace clashes. dbg_ :: (Show a) => Int -> String -> a -> a dbg_ = dbg dbg0 :: Show a => String -> a -> a dbg0 = dbg 0 dbg1 :: Show a => String -> a -> a dbg1 = dbg 1 dbg2 :: Show a => String -> a -> a dbg2 = dbg 2 dbg3 :: Show a => String -> a -> a dbg3 = dbg 3 dbg4 :: Show a => String -> a -> a dbg4 = dbg 4 dbg5 :: Show a => String -> a -> a dbg5 = dbg 5 dbg6 :: Show a => String -> a -> a dbg6 = dbg 6 dbg7 :: Show a => String -> a -> a dbg7 = dbg 7 dbg8 :: Show a => String -> a -> a dbg8 = dbg 8 dbg9 :: Show a => String -> a -> a dbg9 = dbg 9 -- | Like dbg, but sequences properly in IO. dbgIO :: (MonadIO m, Show a) => Int -> String -> a -> m () dbgIO = if shouldLog then plogAtIO else ptraceAtIO dbg0IO :: (MonadIO m, Show a) => String -> a -> m () dbg0IO = dbgIO 0 dbg1IO :: (MonadIO m, Show a) => String -> a -> m () dbg1IO = dbgIO 1 dbg2IO :: (MonadIO m, Show a) => String -> a -> m () dbg2IO = dbgIO 2 dbg3IO :: (MonadIO m, Show a) => String -> a -> m () dbg3IO = dbgIO 3 dbg4IO :: (MonadIO m, Show a) => String -> a -> m () dbg4IO = dbgIO 4 dbg5IO :: (MonadIO m, Show a) => String -> a -> m () dbg5IO = dbgIO 5 dbg6IO :: (MonadIO m, Show a) => String -> a -> m () dbg6IO = dbgIO 6 dbg7IO :: (MonadIO m, Show a) => String -> a -> m () dbg7IO = dbgIO 7 dbg8IO :: (MonadIO m, Show a) => String -> a -> m () dbg8IO = dbgIO 8 dbg9IO :: (MonadIO m, Show a) => String -> a -> m () dbg9IO = dbgIO 9 -- | Like dbg, but with a custom show function. dbgWith :: Int -> (a -> String) -> a -> a dbgWith = if shouldLog then logAtWith else traceAtWith dbg0With :: (a -> String) -> a -> a dbg0With = dbgWith 0 dbg1With :: (a -> String) -> a -> a dbg1With = dbgWith 1 dbg2With :: (a -> String) -> a -> a dbg2With = dbgWith 2 dbg3With :: (a -> String) -> a -> a dbg3With = dbgWith 3 dbg4With :: (a -> String) -> a -> a dbg4With = dbgWith 4 dbg5With :: (a -> String) -> a -> a dbg5With = dbgWith 5 dbg6With :: (a -> String) -> a -> a dbg6With = dbgWith 6 dbg7With :: (a -> String) -> a -> a dbg7With = dbgWith 7 dbg8With :: (a -> String) -> a -> a dbg8With = dbgWith 8 dbg9With :: (a -> String) -> a -> a dbg9With = dbgWith 9 -- | Helper for producing debug messages: -- concatenates a name (eg a function name), -- short description of the value being logged, -- and string representation of the value. -- -- Eg: @let lbl = lbl_ "print"@, -- @dbg1With (lbl "part 1".show) ...@. -- lbl_ :: String -> String -> String -> String lbl_ name desc val = name <> ": " <> desc <> ":" <> " " <> val -- XXX the resulting function is constrained to only one value type -- -- | A helper for defining a local "dbg" function. -- -- Given a debug level and a topic string (eg, a function name), -- -- it generates a function which takes -- -- - a description string, -- -- - a value-to-string show function, -- -- - and a value to be inspected, -- -- debug-logs the topic, description and result of calling the show function on the value, -- -- formatted nicely, at the specified debug level or above, -- -- then returns the value. -- dbg_ :: forall a. Int -> String -> (String -> (a -> String) -> a -> a) -- dbg_ level topic = -- \desc showfn val -> -- dbgWith level (lbl_ topic desc . showfn) val -- {-# HLINT ignore "Redundant lambda" #-} -- | Whether ghc-debug support is included in this build, and if so, how it will behave. -- When hledger is built with the @ghcdebug@ cabal flag (off by default, because of extra deps), -- it can listen (on unix ?) for connections from ghc-debug clients like ghc-debug-brick, -- for pausing/resuming the program and inspecting memory usage and profile information. -- -- With a ghc-debug-supporting build, ghc-debug can be enabled by running hledger with -- a negative --debug level. There are three different modes: -- --debug=-1 - run normally (can be paused/resumed by a ghc-debug client), -- --debug=-2 - pause and await client commands at program start (not useful currently), -- --debug=-3 - pause and await client commands at program end. data GhcDebugMode = GDNotSupported | GDDisabled | GDNoPause | GDPauseAtStart | GDPauseAtEnd -- keep synced with ghcDebugMode deriving (Eq,Ord,Show) -- | Is the hledger-lib package built with ghc-debug support ? ghcDebugSupportedInLib :: Bool ghcDebugSupportedInLib = #ifdef GHCDEBUG True #else False #endif -- | Should the program open a socket allowing control by ghc-debug-brick or similar ghc-debug client ? -- See GhcDebugMode. ghcDebugMode :: GhcDebugMode ghcDebugMode = #ifdef GHCDEBUG case debugLevel of _ | not ghcDebugSupportedInLib -> GDNotSupported (-1) -> GDNoPause (-2) -> GDPauseAtStart (-3) -> GDPauseAtEnd _ -> GDDisabled -- keep synced with GhcDebugMode #else GDNotSupported #endif -- | When ghc-debug support has been built into the program and enabled at runtime with --debug=-N, -- this calls ghc-debug's withGhcDebug; otherwise it's a no-op. withGhcDebug' = #ifdef GHCDEBUG if ghcDebugMode > GDDisabled then withGhcDebug else id #else id #endif -- | When ghc-debug support has been built into the program, this calls ghc-debug's pause, otherwise it's a no-op. ghcDebugPause' :: IO () ghcDebugPause' = #ifdef GHCDEBUG pause #else return () #endif