Извлечение контекста для трассировки / регистрации с помощью haskell метапрограммирования - PullRequest
6 голосов
/ 04 мая 2020

В нашей кодовой базе haskell бизнес-логика c чередуется с кодом отслеживания и регистрации. Это может затмить бизнес-логику c и усложнить ее понимание и отладку. Я ищу идеи о том, как уменьшить объем кода для ведения журнала и трассировки, чтобы бизнес-логика c выделялась больше.

Наш код в настоящее время в основном выглядит примерно так:

someFunction a b cs = 
  withTaggedSpan tracer "TRACE_someFunction" [("arg_b", show b)] $ do
    logDebug logger $ "someFunction start: " <> show (trimDownC <$> cs)
    result <- do ... some business logic ...
    if isError result then
      logError logger $ "someFunction error: " <> show result
    else
      logDebug logger $ "someFunction success: " <> show (trimDownResult result)

Одно наблюдение состоит в том, что в основном отслеживают все тело функции и регистрируют в начале и в конце. Это должно позволить объединить трассировку и вход в единый помощник и автоматически извлекать имя функции и имена захваченных значений посредством метапрограммирования. Ранее я использовал AST для преобразования макросов времени компиляции и самоанализа во время выполнения в других языках, но не Haskell.

Каковы хорошие способы сделать это, используя Template Haskell, HasCallStack или другие опции?

(Крест размещен на https://www.reddit.com/r/haskell/comments/gdfu52/extracting_context_for_tracinglogging_via_haskell/)

Ответы [ 2 ]

0 голосов
/ 05 мая 2020

Для простоты предположим, что функции в вашей бизнес-логике c имеют вид:

_foo :: Int -> String -> ReaderT env IO ()
_bar :: Int -> ExceptT String (ReaderT env IO) Int

То есть они возвращают значения в преобразователе ReaderT через IO, или, возможно, также выбрасывать ошибки, используя ExceptT. (На самом деле, ReaderT трансформатор сейчас не требуется, но он пригодится позже).

Мы могли бы определить функцию traced следующим образом:

{-# LANGUAGE FlexibleInstances #-}
import Data.Void (absurd)
import Control.Monad.IO.Class
import Control.Monad.Reader -- from "mtl"
import Control.Monad.Trans -- from "transformers"
import Control.Monad.Trans.Except

traced :: Traceable t => Name -> t -> t
traced name = _traced name []

type Name = String
type Arg = String

class Traceable t where
  _traced :: Name -> [Arg] -> t -> t

instance Show r => Traceable (ReaderT env IO r) where
  _traced msg args t = either absurd id <$> runExceptT (_traced msg args (lift t))

instance (Show e, Show r) => Traceable (ExceptT e (ReaderT env IO) r) where
  _traced msg args t =
    do
      liftIO $ putStrLn $ msg ++ " invoked with args " ++ show args
      let mapExits m = do
            e <- m
            case e of
              Left err -> do
                liftIO $ putStrLn $ msg ++ " failed with error " ++ show err
                return $ Left err
              Right r -> do
                liftIO $ putStrLn $ msg ++ " exited with value " ++ show r
                return $ Right r
      mapExceptT (mapReaderT mapExits) t

instance (Show arg, Traceable t) => Traceable (arg -> t) where
  _traced msg args f = \arg -> _traced msg (args ++ [show arg]) (f arg)

Это решение все еще немного неудовлетворительно, потому что для функций, которые вызывают другие функции, мы должны с самого начала решить, хотим ли мы отслеживать отслеживаемую версию вызываемых функций или нет.

Одна вещь, которую мы могли бы попробовать - хотя и более агрессивная к коду - это поместить наши функции в запись и сделать среду ReaderT равной этой же записи. Примерно так:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
-- from "red-black-record"
import Data.RBR (FromRecord (..), IsRecordType, ToRecord (..))

data MyAPI = MyAPI
  { foo :: Int -> String -> ReaderT MyAPI IO (),
    bar :: Int -> ExceptT String (ReaderT MyAPI IO) Int,
    baz :: Bool -> ExceptT String (ReaderT MyAPI IO) ()
  }
  deriving (Generic, FromRecord, ToRecord)

Затем используйте некоторую универсальную библиотеку утилит (здесь red-black-record ), чтобы написать функцию, которая говорит: «если каждая функция в вашей записи Traceable, я дам вам еще одну запись, в которой отслеживаются все функции ":

import Data.Kind
import Data.Proxy
import Data.Monoid (Endo(..))
import GHC.TypeLits
import Data.RBR
  ( I (..),
    KeyValueConstraints,
    KeysValuesAll,
    Maplike,
    cpure'_Record,
    liftA2_Record,
  )

traceAPI ::
  ( IsRecordType r t,
    Maplike t,
    KeysValuesAll (KeyValueConstraints KnownSymbol Traceable) t
  ) =>
  r ->
  r
traceAPI =
  let transforms =
        cpure'_Record (Proxy @Traceable) $ 
            \fieldName -> Endo (traced fieldName)
      applyTraced (Endo endo) (I v) = I (endo v)
   in fromRecord . liftA2_Record applyTraced transforms . toRecord

-- small helper function to help invoke the functions in the record
call :: MonadReader env m => (env -> f) -> (f -> m r) -> m r
call getter execute = do
  f <- asks getter
  execute f

В качестве альтернативы, чтобы избежать волхвов c, такую ​​функцию мы могли бы написать вручную для каждой конкретной записи API.

Работаем:

main :: IO ()
main = do
    let api =
          traceAPI $
            MyAPI
              { foo = \_ _ ->
                  do liftIO $ putStrLn "this is foo",
                bar = \_ ->
                  do
                    liftIO $ putStrLn "this is bar"
                    return 5,
                baz = \_ ->
                  do
                    call foo $ \f -> lift $ f 0 "fooarg"
                    call bar $ \f -> f 23
                    throwE "oops"
              }
    flip runReaderT api $ runExceptT $ baz api False
    pure ()
    -- baz invoked with args ["False"]
    -- foo invoked with args ["0","\"fooarg\""]
    -- this is foo
    -- foo exited with value ()
    -- bar invoked with args ["23"]
    -- this is bar
    -- bar exited with value 5
    -- baz failed with error "oops"
0 голосов
/ 04 мая 2020

Чистые функции детерминированы c. Если вы знаете, что вошло в них, вы всегда можете воспроизвести результат. Таким образом, вам не нужно много журналировать внутри основных частей функциональной базы кода.

Регистрируйте только нечистые действия и создавайте свой код в чистом ядре с небольшой императивной оболочкой. Регистрируйте только нечистые действия, которые происходят в оболочке. Я описал эту технику в блоге здесь .

...