packages feed

benign-katip-0.1.0: src/Benign/Katip.hs

{-# LANGUAGE GHC2021 #-}

-- | This module is a small wrapper around
-- [Katip](https://hackage.haskell.org/package/katip) to allow logging in pure
-- code.
--
-- The wrapper is a little primitive still and you will have to handle the
-- transition from IO code to pure code manually. Pull requests are, of course,
-- welcome.
module Benign.Katip
  ( withKatipContext,
    withKatipNamespace,
    withKatip,
    logLocM,
  )
where

import Benign qualified
import GHC.Stack
import Katip qualified
import System.IO.Unsafe (unsafePerformIO)
import Prelude

katipEnv :: Benign.Field Katip.LogEnv
katipEnv = unsafePerformIO Benign.newField
{-# NOINLINE katipEnv #-}

katipContext :: Benign.Field Katip.LogContexts
katipContext = unsafePerformIO Benign.newField
{-# NOINLINE katipContext #-}

katipNamespace :: Benign.Field Katip.Namespace
katipNamespace = unsafePerformIO Benign.newField
{-# NOINLINE katipNamespace #-}

-- | See 'Katip.katipAddContext'.
withKatipContext :: (Katip.LogItem i) => i -> Benign.Strat a -> a -> a
withKatipContext item = Benign.withAltering katipContext (<> Just (Katip.liftPayload item))

-- | See 'Katip.katipAddNamespace'.
withKatipNamespace :: Katip.Namespace -> Benign.Strat a -> a -> a
withKatipNamespace namespace = Benign.withAltering katipNamespace (<> Just namespace)

-- | Within this computation, Katip is configured for pure code.
withKatip ::
  (Katip.LogItem c) =>
  Katip.LogEnv ->
  c ->
  Katip.Namespace ->
  Benign.Strat a ->
  IO a ->
  IO a
withKatip env ctx namespace strat =
  Benign.withSettingIO' katipEnv env
    . Benign.withSettingIO' katipContext (Katip.liftPayload ctx)
    . Benign.withSettingIO katipNamespace namespace strat

-- | @'logLocM' s msg a@ logs a an event, like Katip's 'Katip.logLocM', before
-- evaluating @a@.
logLocM :: forall a. (HasCallStack) => Katip.Severity -> Katip.LogStr -> a -> a
logLocM severity str = withFrozenCallStack spanLog
  where
    -- The whole purpose of naming `span` is to freeze the call stack. It's
    -- important to freeze the call site, so that the log is recorded as
    -- happening on the line where `logLocM` was called, not on this file. But
    -- since `HasCallStack` is magical, it's quite possible for the type system
    -- to “lose track” of the current `HasCallStack` and create a new one from
    -- scratch. This would be invisible. I tried to harden this function by
    -- declaring type signatures everywhere. I haven't tested it yet though. It
    -- may be wrong.
    spanLog :: (HasCallStack) => a -> a
    spanLog = Benign.unsafeSpanBenign doLog (return ()) Benign.whnf

    doLog :: (HasCallStack) => IO ()
    doLog = do
      -- Making an intermediary `KatipContextT` is a little roundabout, but it's
      -- easier than reaching to Katip's internals.
      --
      -- TODO: catch errors
      Just env <- Benign.lookupLexicalState katipEnv
      Just ctx <- Benign.lookupLexicalState katipContext
      Just namespace <- Benign.lookupLexicalState katipNamespace
      Katip.runKatipContextT env ctx namespace $ Katip.logLocM severity str