Search code examples
sqlhaskellloggingpersistentmonad-transformers

filterLogging not working on Database.Persist.Sql's runSqlPool function


The Haskell library Database.Persist.Sqlite includes functions that run within a LoggingT context, to control debugging output. So I expected to be able to limit the debugging output they produce, thus:

runStdoutLoggingT . filterLogger (\_ _ -> False) (runSqlPool (insertBy myData) myPool)

(condensed and simplified from my actual code) However, it doesn't suppress logging. The evalation of insertBy produces a line on stdout of the form

[Debug#SQL] SELECT "id","key","data_source_row_id","loaded" FROM "data_row" WHERE "key"=? AND "data_source_row_id"=?; [PersistText blahblahblah]

So why isn't the output suppressed by the filterLogger call ?

Since the question has received two downvotes, I'll add that the pattern shown above (i.e., runStdoutLoggingT . filterLogger) is used in many GitHub projects and I can't see how my application is any different. It is somewhat frustrating to be downvoted without explanation or means of recourse.


Solution

  • The architecture of Persistent is a little circuitous and underdocumented:

    • withSqlPool takes a builder. The builder is able to build a SqlBackend out of any "logging function" (basically the internal type that MonadLogger uses). The function then creates a resource pool of SqlBackends, for you to acquire and release and use. This is the continuation argument Pool SqlBackend -> m a you pass in. In return, withSqlPool promises to give you back a bunch of side effects, typed as (MonadIO m, MonadBaseControl IO m, MonadLogger m) => m a.

    • runSqlPool, on the other hand, takes a MonadBaseControl IO m => ReaderT SqlBackend m a and a Pool SqlBackend and returns m a. We can infer from this that it basically acquires a SqlBackend from the resource pool, uses it to construct and run a SQL query, and then returns MonadBaseControl IO => m a. Indeed, its documentation is "Get a connection from the pool, run the given action, and then return the connection to the pool."

    Though named similarly, they do two very different things. The first function constructs the resource pool and the second function uses it. Most Persistent SQL code will have this shape:

    withSqlPool (\logFunc -> do
                    conn <- makeConnection connectionString
                    return SqlBackend { ... , connLogFunc = logFunc })
                numberOfOpenConnections
                (\pool -> do
                  runSqlPool (insertBy myData) pool
                  runSqlPool (anotherTransaction moreData) pool)
    

    In fact, if you're using persistent-postgresql, the above is simply the expanded form of

    withPostgresqlPool connectionString
                       numberOfOpenConnections
                       (\pool -> do
                         runSqlPool (insertBy myData) pool
                         runSqlPool (anotherTransaction moreData) pool)
    

    But wait! We can't quite execute this as an IO action yet. MonadIO m, MonadBaseControl IO m, MonadLogger m are our constraints and it's that third one that we have to discharge:

    main :: IO ()
    main =
      runStdoutLoggingT $
        withPostgresqlPool connectionString
                           numberOfOpenConnections
                           (\pool -> do
                             runSqlPool (insertBy myData) pool
                             runSqlPool (anotherTransaction moreData) pool
                             return ())
    

    When the third constraints disappears, we're able to unify IO () with (MonadIO m, MonadBaseControl IO m) => m () by realizing m ~ IO.

    It's now, at this stage, that we're able to insert our filterLogger – right before the constraint is discharged with runStdoutLoggingT:

    main :: IO ()
    main =
      runStdoutLoggingT . filterLogger (\_ _ -> False) $
        withPostgresqlPool connectionString
                           numberOfOpenConnections
                           (\pool -> do
                             runSqlPool (insertBy myData) pool
                             runSqlPool (anotherTransaction moreData) pool
                             return ())
    

    Overall, a mess created by bad naming and the underwhelmingly documented Database.Persist.Sql module.

    Let's underline the point: runSqlPool simply inherits the logging behavior from the MonadLogger constraint generated by withSqlPool. It is only at the withSqlPool level that we're able to insert the desired filterLogger call.