diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# log-base-0.12.0.0 (2022-09-21)
+* Deprecate `withSimpleStdOutLogger` as it's broken in multithreaded environments.
+* Generalize logger related functions to `MonadUnliftIO`.
+* Remove redundant `INLINE` pragmas.
+
 # log-base-0.11.1.0 (2022-04-04)
 * Add support for aeson 2.0.1.0.
 * Add support for GHC 9.2.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,40 @@
-# log-base [![Hackage version](https://img.shields.io/hackage/v/log-base.svg?label=Hackage)](https://hackage.haskell.org/package/log-base) [![Build Status](https://secure.travis-ci.org/scrive/log.svg?branch=master)](http://travis-ci.org/scrive/log)
+# log
 
-Base package for the `log` library suite. Includes only the standard output
-back end. Use this package in conjunction with `log-elasticsearch` or
-`log-postgres`, depending on which back end you need.
+[![Hackage version](https://img.shields.io/hackage/v/log-base.svg?label=Hackage)](https://hackage.haskell.org/package/log-base)
+[![Build Status](https://github.com/scrive/log/workflows/Haskell-CI/badge.svg?branch=master)](https://github.com/scrive/log/actions?query=branch%3Amaster)
+
+A set of libraries that provide a way to record structured log messages with
+multiple backends.
+
+Supported backends:
+
+* Standard output via
+  [`log-base`](https://hackage.haskell.org/package/log-base).
+* Elasticsearch via
+  [`log-elasticsearch`](https://hackage.haskell.org/package/log-elasticsearch).
+* PostgreSQL via
+  [`log-postgres`](https://hackage.haskell.org/package/log-postgres).
+
+## Example
+
+A sample usage for logging to both standard output and Elasticsearch:
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Log
+import Log.Backend.ElasticSearch
+import Log.Backend.StandardOutput
+
+main :: IO ()
+main = do
+  let config = defaultElasticSearchConfig
+        { esServer = "http://localhost:9200"
+        , esIndex  = "logs"
+        }
+  withStdOutLogger $ \stdoutLogger -> do
+    withElasticSearchLogger config $ \esLogger -> do
+      runLogT "main" (stdoutLogger <> esLogger) defaultLogLevel $ do
+        logInfo_ "Hi there"
+```
diff --git a/log-base.cabal b/log-base.cabal
--- a/log-base.cabal
+++ b/log-base.cabal
@@ -1,5 +1,5 @@
 name:                log-base
-version:             0.11.1.0
+version:             0.12.0.0
 synopsis:            Structured logging solution (base package)
 
 description:         A library that provides a way to record structured log
@@ -21,7 +21,7 @@
 build-type:          Simple
 cabal-version:       >=1.10
 extra-source-files:  CHANGELOG.md, README.md
-tested-with:         GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.2
+tested-with:         GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.2
 
 Source-repository head
   Type:     git
@@ -46,7 +46,7 @@
                        deepseq,
                        exceptions >=0.6,
                        mmorph >=1.0.9 && <1.2,
-                       monad-control >=0.3,
+                       monad-control >=1.0.3,
                        mtl,
                        semigroups,
                        stm >=2.4,
@@ -66,7 +66,6 @@
                     , GeneralizedNewtypeDeriving
                     , LambdaCase
                     , MultiParamTypeClasses
-                    , NoImplicitPrelude
                     , OverloadedStrings
                     , RankNTypes
                     , RecordWildCards
diff --git a/src/Log/Backend/LogList.hs b/src/Log/Backend/LogList.hs
--- a/src/Log/Backend/LogList.hs
+++ b/src/Log/Backend/LogList.hs
@@ -9,8 +9,7 @@
   ) where
 
 import Control.Concurrent.MVar
-import System.IO
-import Prelude
+import Control.Monad.IO.Unlift
 
 import Log.Data
 import Log.Internal.Logger
@@ -19,25 +18,27 @@
   deriving Eq
 
 -- | Create a new, empty list.
-newLogList :: IO LogList
-newLogList = LogList <$> newMVar []
+newLogList :: MonadIO m => m LogList
+newLogList = LogList <$> liftIO (newMVar [])
 
 -- | Retrieve messages stored in the list.
-getLogList :: LogList -> IO [LogMessage]
-getLogList (LogList ll) = reverse <$> readMVar ll
+getLogList :: MonadIO m => LogList -> m [LogMessage]
+getLogList (LogList ll) = reverse <$> liftIO (readMVar ll)
 
 -- | Put a message into the list.
-putLogList :: LogList -> LogMessage -> IO ()
-putLogList (LogList ll) msg = modifyMVar_ ll $ \msgs -> return $! msg : msgs
+putLogList :: MonadIO m => LogList -> LogMessage -> m ()
+putLogList (LogList ll) msg = liftIO . modifyMVar_ ll $ \msgs -> return $! msg : msgs
 
 -- | Clear the list.
-clearLogList :: LogList -> IO ()
-clearLogList (LogList ll) = modifyMVar_ ll . const $ return []
+clearLogList :: MonadIO m => LogList -> m ()
+clearLogList (LogList ll) = liftIO . modifyMVar_ ll . const $ return []
 
 -- | Creates a logger that stores messages in the given 'LogList'.
-withLogListLogger :: LogList -> (Logger -> IO r) -> IO r
-withLogListLogger ll = withLogger $ Logger
-  { loggerWriteMessage = putLogList ll
-  , loggerWaitForWrite = return ()
-  , loggerShutdown     = return ()
-  }
+withLogListLogger :: MonadUnliftIO m => LogList -> (Logger -> m r) -> m r
+withLogListLogger ll act = withRunInIO $ \unlift -> withLogger logger (unlift . act)
+  where
+    logger = Logger
+      { loggerWriteMessage = putLogList ll
+      , loggerWaitForWrite = return ()
+      , loggerShutdown     = return ()
+      }
diff --git a/src/Log/Backend/StandardOutput.hs b/src/Log/Backend/StandardOutput.hs
--- a/src/Log/Backend/StandardOutput.hs
+++ b/src/Log/Backend/StandardOutput.hs
@@ -5,8 +5,8 @@
   , withJsonStdOutLogger
   ) where
 
+import Control.Monad.IO.Unlift
 import Data.Aeson
-import Prelude
 import System.IO
 import qualified Data.Text.IO as T
 import qualified Data.ByteString.Lazy.Char8 as BSL
@@ -15,32 +15,24 @@
 import Log.Internal.Logger
 import Log.Logger
 
--- | Create a simple, synchronous logger that prints messages to standard output
--- and flushes 'stdout' on each call to 'loggerWriteMessage' for the duration of
--- the given action.
-withSimpleStdOutLogger :: (Logger -> IO r) -> IO r
-withSimpleStdOutLogger = withLogger $ Logger
-  { loggerWriteMessage = \msg -> do
-      T.putStrLn $ showLogMessage Nothing msg
-      hFlush stdout
-  , loggerWaitForWrite = return ()
-  , loggerShutdown     = return ()
-  }
+withSimpleStdOutLogger :: MonadUnliftIO m => (Logger -> m r) -> m r
+withSimpleStdOutLogger = withStdOutLogger
+{-# DEPRECATED withSimpleStdOutLogger "Use withStdOutLogger instead" #-}
 
 -- | Create a logger that prints messages to standard output for the duration of
 -- the given action.
-withStdOutLogger :: (Logger -> IO r) -> IO r
-withStdOutLogger act = do
+withStdOutLogger :: MonadUnliftIO m => (Logger -> m r) -> m r
+withStdOutLogger act = withRunInIO $ \unlift -> do
   logger <- mkLogger "stdout" $ \msg -> do
     T.putStrLn $ showLogMessage Nothing msg
     hFlush stdout
-  withLogger logger act
+  withLogger logger (unlift . act)
 
 -- | Create a logger that prints messages in the JSON format to standard output
 -- for the duration of the given action.
-withJsonStdOutLogger :: (Logger -> IO r) -> IO r
-withJsonStdOutLogger act = do
+withJsonStdOutLogger :: MonadUnliftIO m => (Logger -> m r) -> m r
+withJsonStdOutLogger act = withRunInIO $ \unlift -> do
   logger <- mkLogger "stdout-json" $ \msg -> do
     BSL.putStrLn $ encode msg
     hFlush stdout
-  withLogger logger act
+  withLogger logger (unlift . act)
diff --git a/src/Log/Backend/StandardOutput/Bulk.hs b/src/Log/Backend/StandardOutput/Bulk.hs
--- a/src/Log/Backend/StandardOutput/Bulk.hs
+++ b/src/Log/Backend/StandardOutput/Bulk.hs
@@ -4,8 +4,8 @@
   , withBulkJsonStdOutLogger
   ) where
 
+import Control.Monad.IO.Unlift
 import Data.Aeson
-import Prelude
 import System.IO (hFlush, stdout)
 import qualified Data.Text.IO as T
 import qualified Data.ByteString.Lazy.Char8 as BSL
@@ -17,23 +17,23 @@
 -- | Create an asynchronouis logger thread that prints messages to standard
 -- output once per second for the duration of the given action. Flushes 'stdout'
 -- on each bulk write.
-withBulkStdOutLogger :: (Logger -> IO r) -> IO r
-withBulkStdOutLogger act = do
+withBulkStdOutLogger :: MonadUnliftIO m => (Logger -> m r) -> m r
+withBulkStdOutLogger act = withRunInIO $ \unlift -> do
   logger <- mkBulkLogger "stdout-bulk"
     (\msgs -> do
         mapM_ (T.putStrLn . showLogMessage Nothing) msgs
         hFlush stdout
     ) (return ())
-  withLogger logger act
+  withLogger logger (unlift . act)
 
 -- | Create a bulk logger that prints messages in the JSON format to standard
 -- output once per second for the duration of the given action. Flushes 'stdout'
 -- on each bulk write.
-withBulkJsonStdOutLogger :: (Logger -> IO r) -> IO r
-withBulkJsonStdOutLogger act = do
+withBulkJsonStdOutLogger :: MonadUnliftIO m => (Logger -> m r) -> m r
+withBulkJsonStdOutLogger act = withRunInIO $ \unlift -> do
   logger <- mkBulkLogger "stdout-bulk-json"
     (\msgs -> do
         mapM_ (BSL.putStrLn . encode) msgs
         hFlush stdout
     ) (return ())
-  withLogger logger act
+  withLogger logger (unlift . act)
diff --git a/src/Log/Backend/Text.hs b/src/Log/Backend/Text.hs
--- a/src/Log/Backend/Text.hs
+++ b/src/Log/Backend/Text.hs
@@ -2,10 +2,8 @@
 -- testing.
 module Log.Backend.Text ( withSimpleTextLogger ) where
 
-import Control.Applicative
+import Control.Monad.IO.Unlift
 import Data.IORef
-import Data.Monoid
-import Prelude
 import qualified Data.Text as T
 import qualified Data.Text.Lazy as L
 import qualified Data.Text.Lazy.Builder as B
@@ -16,8 +14,8 @@
 -- | Create an in-memory logger for the duration of the given action,
 -- returning both the result of the action and the logger's output as
 -- a 'Text' value afterwards.
-withSimpleTextLogger :: (Logger -> IO r) -> IO (T.Text, r)
-withSimpleTextLogger act = do
+withSimpleTextLogger :: MonadUnliftIO m => (Logger -> m r) -> m (T.Text, r)
+withSimpleTextLogger act = withRunInIO $ \unlift -> do
   builderRef <- newIORef mempty
   let logger = Logger
         { loggerWriteMessage = \msg -> do
@@ -26,6 +24,6 @@
         , loggerWaitForWrite = return ()
         , loggerShutdown     = return ()
         }
-  r <- act logger
+  r <- unlift $ act logger
   txt <- L.toStrict . B.toLazyText <$> readIORef builderRef
   return (txt, r)
diff --git a/src/Log/Class.hs b/src/Log/Class.hs
--- a/src/Log/Class.hs
+++ b/src/Log/Class.hs
@@ -15,7 +15,6 @@
 import Data.Aeson
 import Data.Aeson.Types
 import Data.Time
-import Prelude
 import qualified Data.Text as T
 
 import Log.Data
diff --git a/src/Log/Data.hs b/src/Log/Data.hs
--- a/src/Log/Data.hs
+++ b/src/Log/Data.hs
@@ -16,7 +16,6 @@
 import Data.Aeson.Types
 import Data.ByteString.Lazy (toStrict)
 import Data.Time
-import Prelude
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as T
 import qualified Data.Monoid as Monoid
@@ -30,7 +29,6 @@
 -- | This function is partial.
 readLogLevel :: T.Text -> LogLevel
 readLogLevel = either error id . readLogLevelEither
-{-# INLINE readLogLevel #-}
 
 readLogLevelEither :: T.Text -> Either String LogLevel
 readLogLevelEither "attention" = Right LogAttention
diff --git a/src/Log/Internal/Aeson/Compat.hs b/src/Log/Internal/Aeson/Compat.hs
--- a/src/Log/Internal/Aeson/Compat.hs
+++ b/src/Log/Internal/Aeson/Compat.hs
@@ -9,7 +9,6 @@
   ) where
 
 import Data.Text
-import Prelude
 
 #if MIN_VERSION_aeson(2,0,1)
 import Data.Aeson.KeyMap as Map 
diff --git a/src/Log/Internal/Logger.hs b/src/Log/Internal/Logger.hs
--- a/src/Log/Internal/Logger.hs
+++ b/src/Log/Internal/Logger.hs
@@ -8,9 +8,7 @@
   , withLogger
   ) where
 
-import Data.Semigroup
 import Control.Exception
-import Prelude
 
 import Log.Data
 
@@ -50,6 +48,10 @@
 withLogger logger act = act logger `finally` cleanup
   where
     cleanup = waitForLogger logger >> shutdownLogger logger
+-- Prevent GHC from inlining this function so its callers are small and
+-- considered for inlining instead (as they will be generalized to MonadIO or
+-- MonadUnliftIO).
+{-# NOINLINE withLogger #-}
 
 instance Semigroup Logger where
   l1 <> l2 = Logger {
diff --git a/src/Log/Logger.hs b/src/Log/Logger.hs
--- a/src/Log/Logger.hs
+++ b/src/Log/Logger.hs
@@ -11,13 +11,10 @@
   , shutdownLogger
   ) where
 
-import Control.Applicative
 import Control.Concurrent
 import Control.Concurrent.STM
 import Control.Exception
 import Control.Monad
-import Data.Semigroup
-import Prelude
 import qualified Data.Aeson.Types as A
 import qualified Data.Text as T
 import qualified Data.Text.IO as T
@@ -221,3 +218,7 @@
       when (not isEmpty || isInProgress) retry
 
     printLoggerTerminated = T.putStrLn $ name <> ": logger thread terminated"
+-- Prevent GHC from inlining this function so its callers are small and
+-- considered for inlining instead (as they will be generalized to MonadIO or
+-- MonadUnliftIO).
+{-# NOINLINE mkLoggerImpl #-}
diff --git a/src/Log/Monad.hs b/src/Log/Monad.hs
--- a/src/Log/Monad.hs
+++ b/src/Log/Monad.hs
@@ -25,7 +25,6 @@
 import Data.Aeson
 import Data.Text (Text)
 import Data.Time
-import Prelude
 import qualified Control.Monad.Fail as MF
 import qualified Control.Exception as E
 
@@ -115,34 +114,17 @@
     hoist f = mapLogT f
 
 instance MonadTransControl LogT where
-#if MIN_VERSION_monad_control(1,0,0)
   type StT LogT m = StT InnerLogT m
   liftWith = defaultLiftWith LogT unLogT
   restoreT = defaultRestoreT LogT
-#else
-  newtype StT LogT m = StLogT { unStLogT :: StT InnerLogT m }
-  liftWith = defaultLiftWith LogT unLogT StLogT
-  restoreT = defaultRestoreT LogT unStLogT
-#endif
-  {-# INLINE liftWith #-}
-  {-# INLINE restoreT #-}
 
 instance MonadBaseControl b m => MonadBaseControl b (LogT m) where
-#if MIN_VERSION_monad_control(1,0,0)
   type StM (LogT m) a = ComposeSt LogT m a
   liftBaseWith = defaultLiftBaseWith
   restoreM     = defaultRestoreM
-#else
-  newtype StM (LogT m) a = StMLogT { unStMLogT :: ComposeSt LogT m a }
-  liftBaseWith = defaultLiftBaseWith StMLogT
-  restoreM     = defaultRestoreM unStMLogT
-#endif
-  {-# INLINE liftBaseWith #-}
-  {-# INLINE restoreM #-}
 
 instance MonadUnliftIO m => MonadUnliftIO (LogT m) where
   withRunInIO inner = LogT $ withRunInIO $ \run -> inner (run . unLogT)
-  {-# INLINE withRunInIO #-}
 
 instance MonadBase IO m => MonadLog (LogT m) where
   logMessage level message data_ = LogT . ReaderT $ \logEnv -> liftBase $ do
