packages feed

wasmedge-hs-0.17.1.0: src/WasmEdge/Log.hs

{- |
Module      : WasmEdge.Log
Description : WasmEdge runtime logging configuration
Copyright   : (c) 2026 Tushar Adhatrao
License     : MIT
Stability   : stable

Configure the WasmEdge logging system. The logging system is a global
singleton — configuration affects all WasmEdge operations in the process.

Example:

@
import WasmEdge.Log

main :: IO ()
main = do
  setLogLevel Error
  -- ... run WasmEdge operations ...
  logOff  -- silence all output
@
-}
module WasmEdge.Log
  ( -- * Log levels
    LogLevel (..)

    -- * Level control
  , setLogLevel
  , setLogError
  , setLogDebug
  , logOff

    -- * Custom callback
  , LogMessage (..)
  , setLogCallback
  , clearLogCallback
  ) where

import Data.ByteString qualified as BS
import Data.Foldable (for_)
import Data.IORef
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Data.Word (Word32)
import Foreign.C (CString)
import Foreign.Ptr
import Foreign.Storable
import System.IO.Unsafe (unsafePerformIO)

import WasmEdge.Internal.FFI.Basic

-- ---------------------------------------------------------------------------
-- LogLevel
-- ---------------------------------------------------------------------------

-- | The WasmEdge log verbosity level.
data LogLevel
  = -- | Trace-level (most verbose)
    LogTrace
  | -- | Debug-level
    LogDebug
  | -- | Informational
    LogInfo
  | -- | Warnings only
    LogWarn
  | -- | Errors only
    LogError
  | -- | Critical errors only
    LogCritical
  deriving stock (Show, Eq, Ord, Bounded, Enum)

logLevelToC :: LogLevel -> Word32
logLevelToC = \case
  LogTrace -> 0
  LogDebug -> 1
  LogInfo -> 2
  LogWarn -> 3
  LogError -> 4
  LogCritical -> 5

-- ---------------------------------------------------------------------------
-- Level control
-- ---------------------------------------------------------------------------

-- | Set the global log level.
setLogLevel :: LogLevel -> IO ()
setLogLevel = c_LogSetLevel . logLevelToC

-- | Shortcut: set log level to 'LogError'.
setLogError :: IO ()
setLogError = c_LogSetErrorLevel

-- | Shortcut: set log level to 'LogDebug'.
setLogDebug :: IO ()
setLogDebug = c_LogSetDebugLevel

-- | Disable all logging output.
logOff :: IO ()
logOff = c_LogOff

-- ---------------------------------------------------------------------------
-- Custom callback
-- ---------------------------------------------------------------------------

-- | A structured log message delivered to a custom log callback.
data LogMessage = LogMessage
  { logMsgText :: !Text
  -- ^ The log message text.
  , logMsgLogger :: !Text
  -- ^ The name of the logger that produced this message.
  , logMsgLevel :: !LogLevel
  -- ^ The severity level.
  }
  deriving stock (Show, Eq)

-- Global IORef holds the current FunPtr so we can free it when replaced.
currentCallbackRef :: IORef (Maybe (FunPtr RawLogCallbackT))
currentCallbackRef = unsafePerformIO (newIORef Nothing)
{-# NOINLINE currentCallbackRef #-}

{- | Set a custom log callback. The callback replaces any previously set one.
The old callback's 'FunPtr' is freed automatically.

The raw C struct layout of 'WasmEdge_LogMessage' is:

@
typedef struct WasmEdge_LogMessage {
  WasmEdge_String Message;    // offset 0
  WasmEdge_String LoggerName; // offset 16
  WasmEdge_LogLevel Level;    // offset 32
  time_t Time;                // offset 36 (platform-dependent)
  uint64_t ThreadId;          // ...
} WasmEdge_LogMessage;
@

We parse the relevant fields via fixed offsets.
-}
setLogCallback :: (LogMessage -> IO ()) -> IO ()
setLogCallback userCb = do
  -- Wrap the Haskell callback
  rawCb <- mkLogCallback $ \msgPtr -> do
    -- Parse WasmEdge_LogMessage
    -- Message.Length at offset 0, Message.Buf at offset 8
    msgLen <- peekByteOff @Word32 (castPtr msgPtr) 0
    msgBuf <- peekByteOff @CString (castPtr msgPtr) 8
    logLen <- peekByteOff @Word32 (castPtr msgPtr) 16
    logBuf <- peekByteOff @CString (castPtr msgPtr) 24
    lvlW <- peekByteOff @Word32 (castPtr msgPtr) 32
    msgTxt <-
      if msgBuf == nullPtr || msgLen == 0
        then pure mempty
        else TE.decodeUtf8Lenient <$> BS.packCStringLen (msgBuf, fromIntegral msgLen)
    logTxt <-
      if logBuf == nullPtr || logLen == 0
        then pure mempty
        else TE.decodeUtf8Lenient <$> BS.packCStringLen (logBuf, fromIntegral logLen)
    let lvl = toEnum (min (fromIntegral lvlW) (fromEnum LogCritical))
    userCb
      LogMessage
        { logMsgText = msgTxt
        , logMsgLogger = logTxt
        , logMsgLevel = lvl
        }
  -- Free the old callback if any
  old <- readIORef currentCallbackRef
  for_ old freeHaskellFunPtr
  writeIORef currentCallbackRef (Just rawCb)
  c_LogSetCallback rawCb

-- | Remove the custom log callback and restore default logging.
clearLogCallback :: IO ()
clearLogCallback = do
  c_LogSetCallback nullFunPtr
  old <- readIORef currentCallbackRef
  case old of
    Just fp -> freeHaskellFunPtr fp >> writeIORef currentCallbackRef Nothing
    Nothing -> pure ()