packages feed

effectful-tracing-0.1.0.0: src/Effectful/Tracing/Instrumentation/SqliteSimple.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Effectful.Tracing.Instrumentation.SqliteSimple
-- Description : Tracing wrappers for sqlite-simple queries.
-- Copyright   : (c) The effectful-tracing contributors
-- License     : BSD-3-Clause
-- Stability   : experimental
--
-- Drop-in replacements for the @sqlite-simple@ statement runners (@query@,
-- @query_@, @execute@, @execute_@, @executeMany@) that run each call inside a
-- @client@-kind span recording the stable OpenTelemetry database semantic
-- conventions (see "Effectful.Tracing.SemConv"). The wrappers live in @'Eff' es@
-- and delegate to "Effectful.Tracing.Instrumentation.Database", so the span is
-- named after the statement's operation and finalized even if the query throws.
--
-- Import this module qualified alongside @sqlite-simple@ so the traced runners
-- shadow the originals at the call site:
--
-- > import Database.SQLite.Simple (Connection, Only (..))
-- > import Effectful.Tracing.Instrumentation.SqliteSimple qualified as Sqlite
-- >
-- > activeUsers :: (IOE :> es, Tracer :> es) => Connection -> Eff es [(Int, Text)]
-- > activeUsers conn =
-- >   Sqlite.query conn "SELECT id, name FROM users WHERE active = ?" (Only True)
--
-- The recorded @db.query.text@ is the statement /template/ (with @?@
-- placeholders), not the interpolated SQL, so parameter values never reach the
-- span. The @db.operation.name@ is inferred from the leading keyword of that
-- template (see 'inferOperationName'); set @db.collection.name@ \/
-- @db.namespace@ yourself with "Effectful.Tracing.addAttribute" inside the call
-- if you want them, since they cannot be derived reliably without parsing SQL.
module Effectful.Tracing.Instrumentation.SqliteSimple
  ( query
  , query_
  , execute
  , execute_
  , executeMany
  ) where

import Control.Monad.IO.Class (liftIO)
import GHC.Stack (HasCallStack)

import Database.SQLite.Simple (Connection, FromRow, Query, ToRow)
import Database.SQLite.Simple qualified as Sqlite
import Database.SQLite.Simple.Types (fromQuery)

import Effectful (Eff, IOE, (:>))
import Effectful.Tracing (Tracer, addAttribute)
import Effectful.Tracing.Instrumentation.Database
  ( DatabaseQuery (queryOperation, queryText)
  , databaseQuery
  , inferOperationName
  , withQuerySpan
  )
import Effectful.Tracing.SemConv qualified as SemConv

-- | 'Database.SQLite.Simple.query' wrapped in a traced @client@-kind span.
query
  :: (HasCallStack, IOE :> es, Tracer :> es, ToRow q, FromRow r)
  => Connection
  -> Query
  -> q
  -> Eff es [r]
query conn template params =
  withQuerySpan (describe template) (liftIO (Sqlite.query conn template params))

-- | 'Database.SQLite.Simple.query_' wrapped in a traced @client@-kind span.
query_
  :: (HasCallStack, IOE :> es, Tracer :> es, FromRow r)
  => Connection
  -> Query
  -> Eff es [r]
query_ conn template =
  withQuerySpan (describe template) (liftIO (Sqlite.query_ conn template))

-- | 'Database.SQLite.Simple.execute' wrapped in a traced @client@-kind span.
execute
  :: (HasCallStack, IOE :> es, Tracer :> es, ToRow q)
  => Connection
  -> Query
  -> q
  -> Eff es ()
execute conn template params =
  withQuerySpan (describe template) (liftIO (Sqlite.execute conn template params))

-- | 'Database.SQLite.Simple.execute_' wrapped in a traced @client@-kind span.
execute_
  :: (HasCallStack, IOE :> es, Tracer :> es)
  => Connection
  -> Query
  -> Eff es ()
execute_ conn template =
  withQuerySpan (describe template) (liftIO (Sqlite.execute_ conn template))

-- | 'Database.SQLite.Simple.executeMany' wrapped in a traced @client@-kind
-- span. The number of parameter rows is recorded as @db.operation.batch.size@.
executeMany
  :: (HasCallStack, IOE :> es, Tracer :> es, ToRow q)
  => Connection
  -> Query
  -> [q]
  -> Eff es ()
executeMany conn template paramsList =
  withQuerySpan (describe template) $ do
    addAttribute SemConv.dbOperationBatchSize (length paramsList)
    liftIO (Sqlite.executeMany conn template paramsList)

-- | Build the query description from a @sqlite-simple@ 'Query' template: system
-- @\"sqlite\"@, @db.query.text@ from the template, and @db.operation.name@
-- inferred from its leading keyword.
describe :: Query -> DatabaseQuery
describe template =
  (databaseQuery "sqlite")
    { queryText = Just statement
    , queryOperation = inferOperationName statement
    }
  where
    statement = fromQuery template