hledger/hledger-lib/Hledger/Utils/Debug.hs

629 lines
18 KiB
Haskell

{- |
Here are fancier versions of Debug.Trace, with these features:
- short, memorable, greppable function names
- pretty-printing of haskell values, using pretty-simple
- optional ANSI colour
- enabling/disabling debug output with --debug
- debug output levels from 1 to 9, selected by --debug N option
- --debug detected with unsafePerformIO for easy use in pure/IO/startup code
- debug output can be logged instead (for TUI apps)
The "dbgN*" functions are intended to be the most convenient API, to be embedded
at points of interest in your code. They combine the conditional output of "At",
the conditional logging of "OrLog", pretty printing, and short searchable function names.
Parsing the command line, detecting program name, and file logging is done with unsafePerformIO.
If you are working in GHCI, changing the debug level requires editing and reloading this file
(sometimes it's more convenient to add a dbg0 temporarily).
In hledger, debug levels are used as follows:
@
Debug level: What to show:
------------ ---------------------------------------------------------
0 normal command output only (no warnings, eg)
1 useful warnings, most common troubleshooting info (config file args, valuation..)
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
@
We don't yet have the ability to select debug output by topic. For now, here
are some standardish topic strings to search for in hledger debug messages:
acct
arg
budget
calc
csv
journalFinalise
multiBalanceReport
opts
precision
price
q
style
val
-}
-- 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
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
module Hledger.Utils.Debug (
-- * The program's debug level, from 0 (least debug output) to 9 (most).
-- This is parsed from a command line --debug N option, or --debug meaning 1.
-- The command line is read (once) by unsafePerformIO, allowing this to be used
-- easily anywhere in your program.
debugLevel
-- * 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
-- All @dbg*@ functions normally trace to stderr,
-- but they will log to PROGRAM.log instead if the (internal) program name ends with ".log".
-- Eg: @main = withProgName "PROGRAM.log" $ do ...@.
-- This is intended for TUI programs where stderr output is hard to see.
--
-- They have an effect only when the program's debug level is at or above the
-- level specified by an argument or by the function name.
-- The many variants follow a consistent pattern and aim to reduce typing and cognitive load.
-- * Trace/log a string
,dbgMsg
,dbg0Msg
,dbg1Msg
,dbg2Msg
,dbg3Msg
,dbg4Msg
,dbg5Msg
,dbg6Msg
,dbg7Msg
,dbg8Msg
,dbg9Msg
-- * In IO
,dbgMsgIO
,dbg0MsgIO
,dbg1MsgIO
,dbg2MsgIO
,dbg3MsgIO
,dbg4MsgIO
,dbg5MsgIO
,dbg6MsgIO
,dbg7MsgIO
,dbg8MsgIO
,dbg9MsgIO
-- * Trace/log a showable value, pretty-printed
-- @dbg@ here clashes with Text.Megaparsec.Debug (dbg), so that module or this one
-- should be imported qualified if you are using both.
,dbg
,dbg0
,dbg1
,dbg2
,dbg3
,dbg4
,dbg5
,dbg6
,dbg7
,dbg8
,dbg9
-- * In IO
,dbgIO
,dbg0IO
,dbg1IO
,dbg2IO
,dbg3IO
,dbg4IO
,dbg5IO
,dbg6IO
,dbg7IO
,dbg8IO
,dbg9IO
-- * With a custom show function
,dbgWith
,dbg0With
,dbg1With
,dbg2With
,dbg3With
,dbg4With
,dbg5With
,dbg6With
,dbg7With
,dbg8With
,dbg9With
-- * Utilities, ghc-debug
,ghcDebugSupportedInLib
,GhcDebugMode(..)
,ghcDebugMode
,withGhcDebug'
,ghcDebugPause'
-- * Utilities, other
,lbl_
,progName
-- * Re-exports
-- ,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 "<interactive>".
-- 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 programs debug output verbosity. The default is 0 meaning no debug output.
-- The @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to
-- a higher value (the = is required). Uses unsafePerformIO.
-- 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
-- | 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
-- | 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.
-- Traces to stderr or logs to a file depending on shouldLog.
dbg :: (Show a) => Int -> String -> a -> a
dbg = if shouldLog then plogAt else ptraceAt
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 dbgWith, but with a custom show function.
dbgWith :: Int -> (a -> String) -> a -> a
dbgWith = if shouldLog then logAtWith else traceAtWith
-- | Like dbgN, but taking a show function instead of a label.
dbg0With :: (a -> String) -> a -> a
dbg0With = dbgWith 0
dbg1With :: Show a => (a -> String) -> a -> a
dbg1With = dbgWith 1
dbg2With :: Show a => (a -> String) -> a -> a
dbg2With = dbgWith 2
dbg3With :: Show a => (a -> String) -> a -> a
dbg3With = dbgWith 3
dbg4With :: Show a => (a -> String) -> a -> a
dbg4With = dbgWith 4
dbg5With :: Show a => (a -> String) -> a -> a
dbg5With = dbgWith 5
dbg6With :: Show a => (a -> String) -> a -> a
dbg6With = dbgWith 6
dbg7With :: Show a => (a -> String) -> a -> a
dbg7With = dbgWith 7
dbg8With :: Show a => (a -> String) -> a -> a
dbg8With = dbgWith 8
dbg9With :: Show a => (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.
lbl_ :: String -> String -> String -> String
lbl_ name desc val = name <> ": " <> desc <> ":" <> " " <> val
-- XXX the resulting function is constrained to only one value type
-- -- | A new 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. Show 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" #-}