packages feed

yesod-katip-0.1.0.0: src/Yesod/Katip.hs

{-# OPTIONS_GHC -Wno-overlapping-patterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

{-|
Module      : Yesod.Katip
Description : Wrappers for adding automatic Katip integration to Yesod sites.
Copyright   : (c) Isaac van Bakel, 2020
License     : BSD3
Maintainer  : ivb@vanbakel.io
Stability   : experimental
Portability : POSIX

Katip's structured logging is useful, but adding logging after-the-fact to a
Yesod site which already uses the Yesod-provided logging invocations can be a
lot of work.

This module provides several convenience wrappers for converting existing Yesod
sites into Katip-using versions without needing to modify any handlers.
Instead, the wrapped versions will add in HTTP structures like requests, etc.
automatically, and logs sent to Yesod will be intercepted and also sent to
Katip along with any structure.

These wrappers are configurable - they can be made to redirect logs, duplicate
them (sending both to Katip and the Yesod logger), or even ignore them, as
necessary. See 'KatipConfig' for more detail.

If your site has a 'Yesod' instance, so will the wrapped version - so using it
is as simple as passing the wrapped version along to WAI, or whichever server
you use.

There's also support for using Katip's API for more direct control over your
Katip logs inside Yesod handlers. This is based in 'SiteKatip', which is a
ytl-style site class.
-}
module Yesod.Katip
  ( KatipSite (..)
  , KatipContextSite (..)

  , KatipConfig (..)
  , LoggingApproach (..)
  ) where

import Yesod.Katip.Class

import qualified Katip as K
import Network.Wai (Request)
import Yesod.Core
  ( RenderRoute (..)
  , waiRequest
  , Yesod (..)
  )
import Yesod.Core.Types
import Yesod.Site.Class
import Yesod.Site.Util
import Yesod.Trans.Class as ST
import Yesod.Trans.Class.Reader
import Yesod.Trans.TH

import Control.Monad (guard)
import Control.Monad.Logger as L
  ( Loc
  , LogSource
  , LogLevel (..)
  , LogStr
  , fromLogStr
  )
import Data.Bifunctor (second)
import Data.Default
import Data.Maybe (fromMaybe)

-- | Control how the Katip wrapper directs logs that come from Yesod.
--
-- Regardless of the choice of approach, logs will only be sent when
-- @shouldLogIO@ says they should.
data LoggingApproach
  = YesodOnly
  -- ^ Send these logs only to the Yesod logger configured by the site's Yesod
  -- instance already. This is provided only for debugging convenience - it
  -- doesn't make sense to use it in production.
  | KatipOnly
  -- ^ Send these logs only to the Katip scribes, ignoring the Yesod
  -- logger.
  | Both
  -- ^ Send logs to both the Katip scribes and the Yesod logger. If Katip is
  -- configured to log structure as well, this structure *won't* be sent to the
  -- Yesod logger. This is the default.

-- | Configuration for how 'KatipSite' and 'KatipContextSite' turn Yesod logs
-- into Katip ones
data KatipConfig
  = KatipConfig
      { loggingApproach :: LoggingApproach
      -- ^ How logs should be sent between the Yesod logger and your Katip scribes.
      -- See 'LoggingApproach' for details.
      , levelToSeverity :: LogLevel -> K.Severity
      -- ^ How a Yesod level should be translated into a Katip severity.
      , sourceToNamespace :: LogSource -> K.Namespace
      -- ^ How a Yesod log source should modify the Katip namespace. By default,
      -- it is appended on.
      }

instance Default KatipConfig where
  def = KatipConfig
          { loggingApproach = Both 
          , levelToSeverity = defaultLevelToSeverity
          , sourceToNamespace = K.Namespace . pure
          }

defaultLevelToSeverity :: LogLevel -> K.Severity
defaultLevelToSeverity LevelDebug = K.DebugS
defaultLevelToSeverity LevelInfo = K.InfoS
defaultLevelToSeverity LevelWarn = K.WarningS
defaultLevelToSeverity LevelError = K.ErrorS
defaultLevelToSeverity (LevelOther other) = fromMaybe K.ErrorS $ K.textToSeverity other

-------------------
--- CONVERSIONS ---
-------------------

-- Bridge between Yesod-style logging and Katip-style

katipLog :: KatipConfig -> K.LogEnv -> Loc -> LogSource -> LogLevel -> L.LogStr -> IO ()
katipLog KatipConfig{..} logEnv loc source level str = do
  K.runKatipT logEnv do
    K.logItem () (sourceToNamespace source) (Just loc) (levelToSeverity level) (K.logStr $ L.fromLogStr str)

katipLogWithContexts
  :: KatipConfig -> K.LogEnv -> K.LogContexts -> K.Namespace
  -> Loc -> LogSource -> LogLevel -> L.LogStr -> IO ()
katipLogWithContexts KatipConfig{..} logEnv logCtxts namespace loc source level str = do
  K.runKatipContextT logEnv logCtxts namespace do
    K.logItem logCtxts (namespace <> sourceToNamespace source) (Just loc)
      (levelToSeverity level) (K.logStr $ L.fromLogStr str)

---------------
--- LOGGING ---
---------------

-- A Katip wrapper for logging to Katip from Yesod

-- | A wrapper for adding Katip functionality to a site.
--
-- This is the most basic wrapper. It will allow you to redirect logs from
-- Yesod to Katip, as configured.  It will not include HTTP structures in the
-- output - for that, look at 'KatipContextSite' instead.
newtype KatipSite site
  = KatipSite
      { unKatipSite :: ReaderSite (KatipConfig, K.LogEnv) site
      }

instance SiteTrans KatipSite where
  lift = withSiteT unKatipSite . lift

  mapSiteT runner = withSiteT unKatipSite . mapSiteT runner . withSiteT KatipSite

instance (RenderRoute site, Eq (Route site)) => RenderRoute (KatipSite site) where
  newtype Route (KatipSite site) = KRoute (Route (ReaderSite (KatipConfig, K.LogEnv) site))
  renderRoute (KRoute route) = renderRoute route

instance SiteKatip (KatipSite site) where
  getLogEnv = withSiteT unKatipSite $ snd <$> ask

  localLogEnv f = withSiteT unKatipSite . (local $ second f) . withSiteT KatipSite

deriving instance Eq (Route site) => Eq (Route (KatipSite site))

defaultYesodInstanceExcept [| unReaderSite . unKatipSite |] [d|
    instance (SiteCompatible site (KatipSite site), Yesod site, Eq (Route site)) => Yesod (KatipSite site) where
      messageLoggerSource (KatipSite (ReaderSite (config, env) site)) logger loc source level str = do
        shouldLog <- shouldLogIO site source level

        let KatipConfig { loggingApproach } = config
            logYesod = messageLoggerSource site logger loc source level str
            logKatip = do
              guard shouldLog
              katipLog config env loc source level str

        case loggingApproach of
          KatipOnly ->
            logKatip

          YesodOnly ->
            logYesod

          Both -> do
            logKatip
            logYesod
  |]

----------------------------
--- LOGGING WITH CONTEXT ---
----------------------------

-- The same thing again, only this one logs the context

-- | A wrapper for adding Katip functionality to a site.
--
-- This is the more featureful wrapper. It can redirect logs, just like
-- 'KatipSite', but will also augment them with useful HTTP structure from
-- Yesod.
data KatipContextSite site
  = KatipContextSite
      { unKatipContextSite :: ReaderSite (KatipConfig, K.LogEnv, K.LogContexts, K.Namespace) site
      }

instance SiteTrans KatipContextSite where
  lift = withSiteT unKatipContextSite . lift

  mapSiteT runner = withSiteT unKatipContextSite . mapSiteT runner . withSiteT KatipContextSite

instance SiteKatip (KatipContextSite site) where
  getLogEnv = withSiteT unKatipContextSite $ do
    (_, env, _, _) <- ask
    pure env

  localLogEnv f = withSiteT unKatipContextSite . local (\(a, env, c, d) -> (a, f env, c, d)) . withSiteT KatipContextSite

instance SiteKatipContext (KatipContextSite site) where
  getKatipContext = withSiteT unKatipContextSite $ do
    (_, _, ctxt, _) <- ask
    pure ctxt

  localKatipContext f = withSiteT unKatipContextSite . local (\(a, b, ctxt, d) -> (a, b, f ctxt, d)) . withSiteT KatipContextSite

  getKatipNamespace = withSiteT unKatipContextSite $ do
    (_, _, _, ns) <- ask
    pure ns
  localKatipNamespace f = withSiteT unKatipContextSite . local (\(a, b, c, ns) -> (a, b, c, f ns)) . withSiteT KatipContextSite

instance (RenderRoute site, Eq (Route site)) => RenderRoute (KatipContextSite site) where
  newtype Route (KatipContextSite site) = KCRoute (Route (ReaderSite (KatipConfig, K.LogEnv, K.LogContexts, K.Namespace) site))
  renderRoute (KCRoute route) = renderRoute route

deriving instance Eq (Route site) => Eq (Route (KatipContextSite site))

defaultYesodInstanceExcept [| unReaderSite . unKatipContextSite |] [d|
    instance (K.LogItem Request, SiteCompatible site (KatipContextSite site), Yesod site, Eq (Route site))
      => Yesod (KatipContextSite site) where
      messageLoggerSource (KatipContextSite (ReaderSite (config, env, context, namespace) site)) logger loc source level str = do
        shouldLog <- shouldLogIO site source level

        let KatipConfig { loggingApproach } = config
            logYesod = messageLoggerSource site logger loc source level str
            logKatip = do
              guard shouldLog
              katipLogWithContexts config env context namespace loc source level str

        case loggingApproach of
          KatipOnly ->
            logKatip

          YesodOnly ->
            logYesod

          Both -> do
            logKatip
            logYesod

      yesodMiddleware argM = do
        req <- waiRequest
        K.katipAddContext req $ mapSiteT yesodMiddleware argM
  |]