Logjammin’

Discontent with the verbose output of version 2.3.3 of the Haskell Tool Stack when using a Solarized Dark terminal theme took me down the rabbit-hole of logging with the rio and monad-logger packages.

RIO.logDebug

The RIO module exports logDebug:

In respect of the RIO env a type, RIO env is an instance of MonadIO and env (RIO env) is an instance of MonadReader.

The class HasLogFunc env promises method logFuncL :: Lens' env LogFunc, that is, a simple lens (Lens') that gets or sets a LogFunc in env.

stack defines, in various modules, a number of instances for HasLogFunc including BuildConfig, Config, Ctx, DotConfig, EnvConfig, PathInfo, Runner, WithGHC env (subject to constraint HasLogFunc env).

The fields of a Runner value include a GlobalOpts value. The fields of a Config value include a Runner value. The fields of a BuildConfig value include a Config value. The fields of a DotConfig, EnvConfig or PathInfo value include a BuildConfig value. The fields of a Ctx include an EnvConfig value.

RIO.LogFunc

The RIO module exports the type LogFunc but not its constructors, a form of data encapsulation. A LogFunc value can be obtained from newLogFunc :: (MonadIO n, MonadIO m) => LogOptions -> n (LogFunc, m ()) or, more conveniently, the module exports withLogFunc :: MonadUnliftIO m => LogOptions -> (LogFunc -> m a) -> m a.

Both functions require a LogOptions value. Again, the module exports the type but not its constructors. A ‘default’ LogOptions value can be obtained from logOptionsHandle :: MonadIO m => Handle -> Bool -> m LogOptions and the module exports a series of setLog... functions with types of the form option -> LogOption -> LogOption.

Before rio-0.1.18.0 there were no setLog... functions in respect of the colours (strictly, ‘ANSI’ control character sequences) used in the LogFunc action. Some of the default colours chosen did not work well with the Solarized Dark theme (‘bright black’ – base03 – for secondary content and ‘bright green’ – base01 – for highlights). I proposed a pull request that, once accepted, added setLogLevelColors, setLogSecondaryColor (for secondary content) and setLogAccentColors.

Control.Monad.Logger.logDebugNS

The Control.Monad.Logger module of the monad-logger package exports logDebugNS. The ‘N’ in its name indicates non-Template Haskell and the ‘S’ indicates that it takes a ‘source’ (of type LogSource, a synonym for Text) as well as a log message (of type Text).

The class Monad m => MonadLogger m promises method monadLoggerLog :: ToLogStr msg => Loc -> LogSource -> LogLevel -> msg -> m ().

The types RIO.LogLevel and Control.Monad.Logger.LogLevel are isomorphic but different.

The RIO.Orphans module of package rio-orphans exports an instance of MonadLogger for RIO env, subject to the constraint HasLogFunc env.

persistent

stack depends on the pantry package, and both depend on the persistent and persistent-sqlite packages which, in turn, depend on the monad-logger package. In particular, pantry makes use of Database.Persist.Sqlite.withSqliteConnInfo in Pantry.SQLite.initStorage:

This introduces the MonadLogger m constraint that must be satisfied.

stack

An important function in stack-2.3.3 is withRunnerGlobal, exported by module Stack.Runners and used in Main.main. It applies runRIO to a Runner and an ‘inner’ RIO Runner a action. The Runner includes a runnerLogFunc that depends a LogOptions that is a function of a GlobalOpts value.

I modified the function, so that the LogOptions value was also a function of the globalStylesUpdate of the GlobalOpts value. I did so with an additional helper function withNewLogFunc:

Another important function in stack-2.3.3 is configFromConfigMonoid, exported by Stack.Config. It ultimately, via withPantryConfig, applies runRIO to the environment and an ‘inner’ RIO env a action which is a function of a Config value.

The function makes use of GHC’s Record Wild Cards extension to the Haskell language. A Config value includes fields (among others) named configRunner, configPantryConfig and configUserStorage. The Config {..} syntax expands to include Config { ..., configPantryConfig = configPantryConfig, ... } etc.

I modified the configFromConfigMonoid function so that the Config value had a configRunner field with a value that, in turn, had a runnerLongFunc field that was an updated LogFuncvalue.

I also modified the function so that the withPantryConfigaction had a local environment that included the same updated LogFunc value. To do that, I used a helper function withLocalLogFunc:

The modified function was as below (in reality, other changes have been made to the function since stack-2.3.3):