duckdb-simple-0.2.0.0: src/Database/DuckDB/Simple/Logging.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE NamedFieldPuns #-}
{- |
Module : Database.DuckDB.Simple.Logging
Description : High-level wrappers for DuckDB custom log storage.
-}
module Database.DuckDB.Simple.Logging (
LogEntry (..),
registerLogStorage,
) where
import Control.Exception (bracket, mask_)
import Control.Monad (when)
import Data.Ratio ((%))
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Foreign as TextForeign
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Database.DuckDB.FFI
import Database.DuckDB.Simple.Callback (ignoreCallbackExceptions, withCallbackResources)
import Database.DuckDB.Simple.Internal (Connection, peekUtf8CString, throwRegistrationError, withDatabaseHandle)
import Foreign.C.String (CString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, nullFunPtr, nullPtr)
import Foreign.Storable (peek, poke)
-- | A single log event delivered through DuckDB's log-storage callback.
data LogEntry = LogEntry
{ logEntryTimestamp :: !(Maybe UTCTime)
, logEntryLevel :: !Text
, logEntryType :: !Text
, logEntryMessage :: !Text
}
deriving (Eq, Show)
-- | Register a custom log storage callback on the database behind a connection.
registerLogStorage :: Connection -> Text -> (LogEntry -> IO ()) -> IO ()
registerLogStorage conn name callback = do
when (Text.null name || Text.any (== '\0') name) $
throwRegistrationError "invalid log storage name"
bracket c_duckdb_create_log_storage destroyLogStorage \storage -> do
when (storage == nullPtr) $ throwRegistrationError "allocate log storage"
withCallbackResources
(\allocate -> allocate (mkWriteLogEntryCallback (logStorageHandler callback)))
(c_duckdb_log_storage_set_extra_data storage)
\writeCb -> do
TextForeign.withCString name $ c_duckdb_log_storage_set_name storage
c_duckdb_log_storage_set_write_log_entry storage writeCb
withDatabaseHandle conn \db -> mask_ do
rc <- c_duckdb_register_log_storage db storage
-- DuckDB consumes extra data on duplicate-name failure.
-- Clear the wrapper to prevent a second destruction.
c_duckdb_log_storage_set_extra_data storage nullPtr nullFunPtr
when (rc /= DuckDBSuccess) $ throwRegistrationError "register log storage"
logStorageHandler ::
(LogEntry -> IO ()) ->
Ptr () ->
Ptr DuckDBTimestamp ->
CString ->
CString ->
CString ->
IO ()
logStorageHandler callback _ timestampPtr levelPtr logTypePtr messagePtr =
ignoreCallbackExceptions do
entry <- do
logEntryTimestamp <- readTimestamp timestampPtr
logEntryLevel <- readCStringText levelPtr
logEntryType <- readCStringText logTypePtr
logEntryMessage <- readCStringText messagePtr
pure LogEntry{logEntryTimestamp, logEntryLevel, logEntryType, logEntryMessage}
callback entry
readTimestamp :: Ptr DuckDBTimestamp -> IO (Maybe UTCTime)
readTimestamp ptr
| ptr == nullPtr = pure Nothing
| otherwise = do
DuckDBTimestamp micros <- peek ptr
pure (Just (posixSecondsToUTCTime (fromRational (toInteger micros % 1000000))))
readCStringText :: CString -> IO Text
readCStringText ptr
| ptr == nullPtr = pure Text.empty
| otherwise = peekUtf8CString ptr
destroyLogStorage :: DuckDBLogStorage -> IO ()
destroyLogStorage storage =
alloca \ptr -> poke ptr storage >> c_duckdb_destroy_log_storage ptr
foreign import ccall "wrapper"
mkWriteLogEntryCallback ::
(Ptr () -> Ptr DuckDBTimestamp -> CString -> CString -> CString -> IO ()) ->
IO DuckDBLoggerWriteLogEntryFun