My Haskell webapp starts with a particular logging configuration. Once the webserver starts, the logging configuration cannot be changed. Here's what the logging config controls:
- logging output format: text logs (mostly used for local dev) vs JSON logs (mostly used for production env)
- whether to compute and emit aggregate metrics for each req/res, like number of SQL statements, number of downstream HTTP requests made, time spent in SQL, time spent in API calls, etc.
- log verbosity: in development env the actual SQL statements executed, detailed req/res logs of API calls made to downstream systems, etc. are logged. In production environment, these are skipped.
The naive way of writing a logging function would check, for every log statement, the logging config to determine (a) whether to output the log at all, (b) which format to use - text vs json, and (c) whether to update the aggregate metrics
Somehow this seems very inefficient to me.
Is it possible to efficiently do the following:
- upon application startup, depending upon the logging config, choose one of several implementations
- somehow inject this implementation into the application's monad (which might be an instance of
MonadLogger
), such that the compiler is still able to do any optimizations that it would have done otherwise.
CodePudding user response:
Sure. Compare the following two functions:
convertData :: Loggable a => Bool -> a -> ByteString
convertData b x = case b of
False -> viaString x
True -> viaJSON x
convertCode :: Loggable a => Bool -> a -> ByteString
convertCode b = case b of
False -> \x -> viaString x
True -> \x -> viaJSON x
In GHC, if you write convertData False
, then it will check and re-check this Bool
on each call; while if you write convertCode False
, the Bool
is checked once (and a function that has already "internalized" that Bool
is returned).
This generalizes to your other requirements as well. Simply write your logging function as a function which takes a logging config, does all its case
statements and other checks immediately, then in each resulting case returns a specialized logging function that accepts the remaining arguments.
(Why the names convertData
and convertCode
? Because they are two versions of a convert
function; one stores its config explicitly as closed-over data, while the other stores its config implicitly in the program counter.)
CodePudding user response:
The basic approach you propose, where you do all the checks in the logging action has essentially this form:
log :: IO ()
log = do
b <- isLogEnabled
if b
then actuallyLog
else return () -- do nothing
main :: IO ()
main = do
let config = Config { logAction = log , ... }
runServer config
You can, and should, move the check isLogEnabled
in the main action:
actuallyLog :: IO ()
actuallyLog = do ...
doNothing :: IO ()
doNothing = return ()
main :: IO ()
main = do
b <- isLogEnabled -- once and for all
let log :: IO ()
log = if b then actuallyLog else doNothing
let config = Config { logAction = log , ... }
runServer config
The if
inside log
will also be evaluated only once, effectively defining log = actuallyLog
or log = doNothing
with no overhead (after the first use).