packages feed

hs-opentelemetry-instrumentation-persistent-0.0.1.0: src/OpenTelemetry/Instrumentation/Persistent.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module OpenTelemetry.Instrumentation.Persistent
  ( wrapSqlBackend
  ) where
import OpenTelemetry.Trace.Core
import OpenTelemetry.Context
import Data.Acquire.Internal
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Database.Persist.Sql
import Database.Persist.SqlBackend (setConnHooks, emptySqlBackendHooks, MkSqlBackendArgs (connRDBMS), getRDBMS, getConnVault, modifyConnVault)
import Database.Persist.SqlBackend.Internal
import Control.Monad.IO.Class
import System.IO.Unsafe (unsafePerformIO)
import qualified Data.Vault.Strict as Vault
import OpenTelemetry.Attributes (Attributes)
import OpenTelemetry.Resource
import UnliftIO.Exception
import OpenTelemetry.Trace.Monad (MonadTracer(..))
import Control.Monad.Reader
import qualified Data.Text as T
import OpenTelemetry.Context.ThreadLocal (getContext, adjustContext)

instance {-# OVERLAPS #-} MonadTracer m => MonadTracer (ReaderT SqlBackend m) where
  getTracer = lift OpenTelemetry.Trace.Monad.getTracer
instance {-# OVERLAPS #-} MonadTracer m => MonadTracer (ReaderT SqlReadBackend m) where
  getTracer = lift OpenTelemetry.Trace.Monad.getTracer
instance {-# OVERLAPS #-} MonadTracer m => MonadTracer (ReaderT SqlWriteBackend m) where
  getTracer = lift OpenTelemetry.Trace.Monad.getTracer

originalConnectionKey :: Vault.Key SqlBackend
originalConnectionKey = unsafePerformIO Vault.newKey
{-# NOINLINE originalConnectionKey #-}

insertOriginalConnection :: SqlBackend -> SqlBackend -> SqlBackend
insertOriginalConnection conn original = modifyConnVault (Vault.insert originalConnectionKey original) conn

lookupOriginalConnection :: SqlBackend -> Maybe SqlBackend
lookupOriginalConnection = Vault.lookup originalConnectionKey . getConnVault

connectionLevelAttributesKey :: Vault.Key [(Text, Attribute)]
connectionLevelAttributesKey = unsafePerformIO Vault.newKey
{-# NOINLINE connectionLevelAttributesKey #-}

-- | Wrap a 'SqlBackend' with appropriate tracing context and attributes
-- so that queries are tracked appropriately in the tracing hierarchy.
wrapSqlBackend
  :: MonadIO m
  => [(Text, Attribute)]
  -- ^ Attributes that are specific to providers like MySQL, PostgreSQL, etc.
  -> SqlBackend
  -> m SqlBackend
wrapSqlBackend attrs conn_ = do
  tp <- getGlobalTracerProvider
  let conn = Data.Maybe.fromMaybe conn_ (lookupOriginalConnection conn_)
  -- TODO add schema to tracerOptions?
  let t = makeTracer tp "hs-opentelemetry-persistent" tracerOptions
  let hooks = emptySqlBackendHooks
        { hookGetStatement = \conn sql stmt -> do
            pure $ Statement
              { stmtQuery = \ps -> do
                  ctxt <- getContext
                  let spanCreator = do
                        s <- createSpan
                          t
                          ctxt
                          sql
                          (defaultSpanArguments { kind = Client, attributes = ("db.statement", toAttribute sql) : attrs })
                        adjustContext (insertSpan s)
                        pure (lookupSpan ctxt, s)
                      spanCleanup (parent, s) = do
                        s `endSpan` Nothing
                        adjustContext $ \ctx ->
                          maybe ctx (`insertSpan` ctx) parent

                  (p, child) <- mkAcquire spanCreator spanCleanup

                  annotateBasics child conn
                  case stmtQuery stmt ps of
                    Acquire stmtQueryAcquireF -> Acquire $ \f ->
                      handleAny
                        (\(SomeException err) -> do
                          recordException child [] Nothing err
                          endSpan child Nothing
                          throwIO err
                        )
                        (stmtQueryAcquireF f)

              , stmtExecute = \ps -> do
                inSpan' t sql (defaultSpanArguments { kind = Client, attributes = ("db.statement", toAttribute sql) : attrs }) $ \s -> do
                  annotateBasics s conn
                  stmtExecute stmt ps
              , stmtReset = stmtReset stmt
              , stmtFinalize = stmtFinalize stmt
              }
        }

  let conn' = conn
        { connHooks = hooks
        , connBegin = \f mIso -> do
            let statement = "begin transaction" <> case mIso of
                  Nothing -> mempty
                  Just ReadUncommitted -> " isolation level read uncommitted"
                  Just ReadCommitted -> " isolation level read committed"
                  Just RepeatableRead -> " isolation level repeatable read"
                  Just Serializable -> " isolation level serializable"
            let attrs' = ("db.statement", toAttribute statement) : attrs
            inSpan' t statement (defaultSpanArguments { kind = Client, attributes = attrs' }) $ \s -> do
              annotateBasics s conn
              connBegin conn f mIso
        , connCommit = \f -> do
            inSpan' t "commit" (defaultSpanArguments { kind = Client, attributes = ("db.statement", toAttribute ("commit" :: Text)): attrs }) $ \s -> do
              annotateBasics s conn
              connCommit conn f
        , connRollback = \f -> do
            inSpan' t "rollback" (defaultSpanArguments { kind = Client, attributes = ("db.statement", toAttribute ("rollback" :: Text)): attrs }) $ \s -> do
              annotateBasics s conn
              connRollback conn f
        , connClose = do
            inSpan' t "close connection" (defaultSpanArguments { kind = Client, attributes = attrs }) $ \s -> do
              annotateBasics s conn
              connClose conn
        }
  pure $ insertOriginalConnection conn' conn

annotateBasics :: MonadIO m => Span -> SqlBackend -> m ()
annotateBasics span conn = do
  addAttributes span
    [ ("db.system", toAttribute $ getRDBMS conn)
    ]