effectful-tracing-0.1.0.0: test/Effectful/Tracing/Instrumentation/DatabaseSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Effectful.Tracing.Instrumentation.DatabaseSpec
-- Description : Tests for the framework-agnostic database span helpers.
--
-- These exercise the pure helpers ('inferOperationName', 'querySpanName',
-- 'queryAttributes') directly and run 'withQuerySpan' through the in-memory
-- interpreter, asserting the emitted span is @client@-kind, named by the
-- convention, and carries exactly the @db.*@ attributes for the fields that were
-- set. No live database is involved: the @postgresql-simple@ wrappers are thin
-- delegations to this core, which is what needs covering.
module Effectful.Tracing.Instrumentation.DatabaseSpec
( tests
) where
import Effectful (Eff, IOE, runEff)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Effectful.Tracing (Tracer, withSpan)
import Effectful.Tracing.Attribute (Attribute (attributeKey, attributeValue), AttributeValue (AttrText))
import Effectful.Tracing.Instrumentation.Database
( DatabaseQuery (queryCollection, queryNamespace, queryOperation, queryText)
, databaseQuery
, inferOperationName
, queryAttributes
, querySpanName
, withQuerySpan
)
import Effectful.Tracing.Internal.Types (Span, SpanKind (Client), spanAttributes, spanKind, spanName)
import Effectful.Tracing.Interpreter.InMemory
( newCapturedSpans
, readCapturedSpans
, runTracerInMemory
)
tests :: TestTree
tests =
testGroup
"Instrumentation.Database"
[ testGroup "inferOperationName" inferOperationCases
, testGroup "querySpanName" spanNameCases
, testGroup "queryAttributes" attributeCases
, testGroup "withQuerySpan" withQuerySpanCases
]
inferOperationCases :: [TestTree]
inferOperationCases =
[ testCase "upper-cases the leading keyword" $
inferOperationName "select * from users" @?= Just "SELECT"
, testCase "keeps an already-upper keyword" $
inferOperationName "INSERT INTO orders VALUES (?)" @?= Just "INSERT"
, testCase "ignores leading whitespace" $
inferOperationName " \n delete from sessions" @?= Just "DELETE"
, testCase "blank statement yields Nothing" $
inferOperationName " " @?= Nothing
, testCase "empty statement yields Nothing" $
inferOperationName "" @?= Nothing
]
spanNameCases :: [TestTree]
spanNameCases =
[ testCase "operation and collection" $
querySpanName
(databaseQuery "postgresql") {queryOperation = Just "SELECT", queryCollection = Just "users"}
@?= "SELECT users"
, testCase "operation only" $
querySpanName (databaseQuery "postgresql") {queryOperation = Just "SELECT"} @?= "SELECT"
, testCase "falls back to the system name" $
querySpanName (databaseQuery "postgresql") @?= "postgresql"
, testCase "collection without operation falls back to the system name" $
querySpanName (databaseQuery "postgresql") {queryCollection = Just "users"} @?= "postgresql"
]
attributeCases :: [TestTree]
attributeCases =
[ testCase "system only when nothing else is set" $
keys (databaseQuery "postgresql") @?= ["db.system.name"]
, testCase "includes every set optional field in convention order" $
keys
(databaseQuery "postgresql")
{ queryText = Just "SELECT 1"
, queryOperation = Just "SELECT"
, queryCollection = Just "users"
, queryNamespace = Just "app"
}
@?= [ "db.system.name"
, "db.query.text"
, "db.operation.name"
, "db.collection.name"
, "db.namespace"
]
, testCase "omits unset optional fields" $
keys (databaseQuery "postgresql") {queryOperation = Just "SELECT"}
@?= ["db.system.name", "db.operation.name"]
, testCase "records the system name value" $
lookup "db.system.name" (pairs (databaseQuery "mysql")) @?= Just (AttrText "mysql")
]
where
keys = map attributeKey . queryAttributes
pairs = map (\a -> (attributeKey a, attributeValue a)) . queryAttributes
withQuerySpanCases :: [TestTree]
withQuerySpanCases =
[ testCase "emits a client-kind span named by the convention" $ do
spans <-
run $
withQuerySpan
(databaseQuery "postgresql") {queryOperation = Just "SELECT", queryCollection = Just "users"}
(pure ())
case spans of
[s] -> do
spanName s @?= "SELECT users"
spanKind s @?= Client
other -> fail ("expected exactly one captured span, got " <> show (length other))
, testCase "annotates the span with the db.* attributes" $ do
spans <-
run $
withQuerySpan
(databaseQuery "postgresql") {queryText = Just "SELECT 1", queryOperation = Just "SELECT"}
(pure ())
case spans of
[s] ->
map attributeKey (spanAttributes s)
@?= ["db.system.name", "db.query.text", "db.operation.name"]
other -> fail ("expected exactly one captured span, got " <> show (length other))
, testCase "nests under an enclosing span" $ do
spans <- run (withSpan "outer" (withQuerySpan (databaseQuery "postgresql") (pure ())))
map spanName spans @?= ["postgresql", "outer"]
]
-- | Run a 'Tracer' computation through the in-memory interpreter, returning the
-- captured spans (innermost first, as the interpreter records them on close).
run :: Eff '[Tracer, IOE] a -> IO [Span]
run action = runEff $ do
captured <- newCapturedSpans
_ <- runTracerInMemory captured action
readCapturedSpans captured