packages feed

duckdb-simple-0.1.5.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, onException)
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.Internal (Connection, mkDeleteCallback, throwRegistrationError, withDatabaseHandle)
import Foreign.C.String (CString, peekCString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, freeHaskellFunPtr, nullPtr)
import Foreign.StablePtr (StablePtr, castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, freeStablePtr, newStablePtr)
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
    writeCb <- mkWriteLogEntryCallback (logStorageHandler callback)
    callbackStable <- newStablePtr writeCb
    deleteCb <- mkDeleteCallback releaseWriteLogCallback
    let release = freeHaskellFunPtr writeCb >> freeStablePtr callbackStable >> freeHaskellFunPtr deleteCb
    bracket c_duckdb_create_log_storage destroyLogStorage \storage ->
        (`onException` release) $ do
            TextForeign.withCString name \cName ->
                c_duckdb_log_storage_set_name storage cName
            c_duckdb_log_storage_set_write_log_entry storage writeCb
            c_duckdb_log_storage_set_extra_data storage (castStablePtrToPtr callbackStable) deleteCb
            withDatabaseHandle conn \db -> do
                rc <- c_duckdb_register_log_storage db storage
                if rc == DuckDBSuccess
                    then pure ()
                    else throwRegistrationError "register log storage"

logStorageHandler ::
    (LogEntry -> IO ()) ->
    Ptr () ->
    Ptr DuckDBTimestamp ->
    CString ->
    CString ->
    CString ->
    IO ()
logStorageHandler callback _ timestampPtr levelPtr logTypePtr messagePtr = 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 = Text.pack <$> peekCString ptr

releaseWriteLogCallback :: Ptr () -> IO ()
releaseWriteLogCallback rawPtr =
    if rawPtr == nullPtr
        then pure ()
        else do
            let stablePtr = castPtrToStablePtr rawPtr :: StablePtr DuckDBLoggerWriteLogEntryFun
            callback <- deRefStablePtr stablePtr
            freeHaskellFunPtr callback
            freeStablePtr stablePtr

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