packages feed

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

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

-- |
-- Module      : Effectful.Tracing.Instrumentation.Valiant
-- Description : Tracing wrappers for valiant's Effectful adapter.
-- Copyright   : (c) The effectful-tracing contributors
-- License     : BSD-3-Clause
-- Stability   : experimental
--
-- Drop-in replacements for the statement runners in @Valiant.Effectful@ (the
-- @valiant-effectful@ adapter for <https://hackage.haskell.org/package/valiant valiant>,
-- the compile-time checked PostgreSQL library) that run each call inside a
-- @client@-kind span recording the stable OpenTelemetry database semantic
-- conventions (see "Effectful.Tracing.SemConv"). Each wrapper delegates to the
-- @Valiant@ effect and 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 /instead of/ @Valiant.Effectful@ for the traced
-- runners, keeping @Valiant.Effectful@ for the handler ('Valiant.Effectful.runValiant')
-- and the transaction \/ raw-connection helpers, which carry no statement to
-- describe:
--
-- > import Valiant (Statement)
-- > import Valiant.Effectful (Valiant, runValiant)
-- > import Effectful.Tracing.Instrumentation.Valiant qualified as V
-- >
-- > activeUsers :: (Valiant :> es, Tracer :> es) => Statement () User -> Eff es [User]
-- > activeUsers listUsers = V.fetchAllEff listUsers ()
--
-- The system name is @postgresql@. The recorded @db.query.text@ is the
-- statement's own validated SQL (a 'Valiant.Statement', whose text is the
-- parameterized query, never interpolated values), and @db.operation.name@ is
-- inferred from its leading keyword (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.Valiant
  ( -- * Queries
    fetchOneEff
  , fetchAllEff
  , fetchScalarEff
  , fetchOneOrThrowEff
  , fetchExistsEff

    -- * Commands
  , executeEff
  , executeReturningEff
  , executeBatchEff
  ) where

import Data.Int (Int64)
import Data.Text.Encoding (decodeUtf8Lenient)
import GHC.Stack (HasCallStack)

import Valiant (Statement (stmtSQL))
import Valiant.Effectful (Valiant)
import Valiant.Effectful qualified as V

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

-- | 'Valiant.Effectful.fetchOneEff' wrapped in a traced @client@-kind span.
fetchOneEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p r
  -> p
  -> Eff es (Maybe r)
fetchOneEff stmt params =
  withQuerySpan (describe stmt) (V.fetchOneEff stmt params)

-- | 'Valiant.Effectful.fetchAllEff' wrapped in a traced @client@-kind span.
fetchAllEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p r
  -> p
  -> Eff es [r]
fetchAllEff stmt params =
  withQuerySpan (describe stmt) (V.fetchAllEff stmt params)

-- | 'Valiant.Effectful.fetchScalarEff' wrapped in a traced @client@-kind span.
fetchScalarEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p r
  -> p
  -> Eff es r
fetchScalarEff stmt params =
  withQuerySpan (describe stmt) (V.fetchScalarEff stmt params)

-- | 'Valiant.Effectful.fetchOneOrThrowEff' wrapped in a traced @client@-kind span.
fetchOneOrThrowEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p r
  -> p
  -> Eff es r
fetchOneOrThrowEff stmt params =
  withQuerySpan (describe stmt) (V.fetchOneOrThrowEff stmt params)

-- | 'Valiant.Effectful.fetchExistsEff' wrapped in a traced @client@-kind span.
fetchExistsEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p r
  -> p
  -> Eff es Bool
fetchExistsEff stmt params =
  withQuerySpan (describe stmt) (V.fetchExistsEff stmt params)

-- | 'Valiant.Effectful.executeEff' wrapped in a traced @client@-kind span.
executeEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p ()
  -> p
  -> Eff es Int64
executeEff stmt params =
  withQuerySpan (describe stmt) (V.executeEff stmt params)

-- | 'Valiant.Effectful.executeReturningEff' wrapped in a traced @client@-kind span.
executeReturningEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p r
  -> p
  -> Eff es (Int64, [r])
executeReturningEff stmt params =
  withQuerySpan (describe stmt) (V.executeReturningEff stmt params)

-- | 'Valiant.Effectful.executeBatchEff' wrapped in a traced @client@-kind span.
-- The number of parameter rows is recorded as @db.operation.batch.size@.
executeBatchEff
  :: (HasCallStack, Valiant :> es, Tracer :> es)
  => Statement p ()
  -> [p]
  -> Eff es Int64
executeBatchEff stmt paramsList =
  withQuerySpan (describe stmt) $ do
    addAttribute SemConv.dbOperationBatchSize (length paramsList)
    V.executeBatchEff stmt paramsList

-- | Build the query description from a 'Valiant.Statement': system
-- @\"postgresql\"@, @db.query.text@ from the statement's validated SQL, and
-- @db.operation.name@ inferred from its leading keyword.
describe :: Statement p r -> DatabaseQuery
describe stmt =
  (databaseQuery "postgresql")
    { queryText = Just statement
    , queryOperation = inferOperationName statement
    }
  where
    statement = decodeUtf8Lenient (stmtSQL stmt)