otel-effectful-1.0.0: test/Effectful/OpenTelemetry/TracingSpec.hs
{- HLINT ignore "Use head" -}
{-# LANGUAGE OverloadedLists #-}
module Effectful.OpenTelemetry.TracingSpec where
import Arbitrary (Marker (..))
import Control.Monad (forM_)
import Data.Either (isLeft)
import Data.Functor ((<&>))
import Data.Text qualified as Text
import Effectful
import Effectful.Concurrent (runConcurrent)
import Effectful.Concurrent.Async (concurrently_, replicateConcurrently_)
import Effectful.Error.Static (runErrorNoCallStack, throwError)
import Effectful.Exception (AssertionFailed (..), throwIO, try)
import Effectful.HUnit (HUnit)
import Effectful.Hspec
import Effectful.OpenTelemetry.Tracing
import Effectful.OpenTelemetry.Tracing.Span.Context qualified as Span.Context
import Effectful.OpenTelemetry.Tracing.Span.Event (Event (..))
import Effectful.OpenTelemetry.Tracing.Span.Kind qualified as Span.Kind
import Effectful.OpenTelemetry.Tracing.Span.Status qualified as Span.Status
import Util
import Prelude
spec :: (IOE :> es, HUnit :> es, Hspec :> es) => Eff es ()
spec = runConcurrent . parallel $ do
describe "inSpan" do
prop "exports a span" \(Marker name) spanKind a -> do
s <- only . snd =<< (runInMemoryTracing . inSpan name spanKind a . pure) ()
s.name `shouldBe` name
s.kind `shouldBe` spanKind
s.attributes `shouldBe` a
s.endTime `shouldSatisfy` (>= Just s.startTime)
it "no parent, unique ids" do
(_, spans) <- runInMemoryTracing do
inSpan "a" Span.Kind.Internal mempty $ pure ()
inSpan "b" Span.Kind.Internal mempty $ pure ()
inSpan "c" Span.Kind.Internal mempty $ pure ()
mapM_ ((`shouldBe` Nothing) . (.parentSpanId)) spans
(spans <&> (.context.spanId)) `shouldSatisfy` allUnique
describe "nesting" do
it "parent-child chain" do
(_, spans) <- runInMemoryTracing do
inSpan "root" Span.Kind.Internal mempty
. inSpan "middle" Span.Kind.Internal mempty
. inSpan "leaf" Span.Kind.Internal mempty
$ pure ()
length spans `shouldBe` 3
let leaf = spans !! 0
middle = spans !! 1
root = spans !! 2
leaf.parentSpanId `shouldBe` Just middle.context.spanId
middle.parentSpanId `shouldBe` Just root.context.spanId
root.parentSpanId `shouldBe` Nothing
it "shared parent" do
(_, spans) <- runInMemoryTracing do
inSpan "parent" Span.Kind.Internal mempty do
inSpan "child-a" Span.Kind.Internal mempty $ pure ()
inSpan "child-b" Span.Kind.Internal mempty $ pure ()
length spans `shouldBe` 3
let childA = spans !! 0
childB = spans !! 1
parent = spans !! 2
childA.parentSpanId `shouldBe` Just parent.context.spanId
childB.parentSpanId `shouldBe` Just parent.context.spanId
it "restores parent context" do
(ctx, _) <- runInMemoryTracing do
inSpan "outer" Span.Kind.Internal mempty do
inSpan "inner" Span.Kind.Internal mempty $ pure ()
currentContext
ctx `shouldSatisfy` \case
Just _ -> True
Nothing -> False
describe "concurrency" do
it "no loss, unique ids" do
let n = 500 :: Int
(_, spans) <-
runInMemoryTracing
. runConcurrent
. replicateConcurrently_ n
. inSpan "parallel" Span.Kind.Internal mempty
$ pure ()
length spans `shouldBe` n
(spans <&> (.context.spanId)) `shouldSatisfy` allUnique
it "concurrent nesting" do
(_, spans) <-
runInMemoryTracing
. runConcurrent
. inSpan "root" Span.Kind.Internal mempty
$ concurrently_
(inSpan "branch-a" Span.Kind.Internal mempty $ pure ())
(inSpan "branch-b" Span.Kind.Internal mempty $ pure ())
length spans `shouldBe` 3
case filter (("root" ==) . (.name)) spans of
[root] -> do
let branches = filter ((/= "root") . (.name)) spans
mapM_ ((`shouldBe` Just root.context.spanId) . (.parentSpanId)) branches
_ -> expectationFailure "expected exactly one root span"
describe "exception handling" do
prop "Error status on exception" \(Marker msg) -> do
(result, spans) <-
runInMemoryTracing
. try @AssertionFailed
. inSpan @_ @() "item" Span.Kind.Internal mempty
. throwIO
$ AssertionFailed (Text.unpack msg)
result `shouldSatisfy` isLeft
length spans `shouldBe` 1
forM_ spans \s -> do
s.status.code `shouldBe` Span.Status.Error
s.status.message `shouldSatisfy` Text.isPrefixOf msg
length s.events `shouldBe` 1
fmap (.name) s.events `shouldBe` ["exception"]
describe "events" do
prop "records events in order" \(Marker n1) a1 (Marker n2) a2 -> do
s <-
only . snd =<< runInMemoryTracing do
inSpan "test-span" Span.Kind.Internal mempty do
addEventNow n1 a1
addEventNow n2 a2
pure ()
fmap (\e -> (e.name, e.attributes)) s.events `shouldBe` [(n1, a1), (n2, a2)]
it "events go to innermost span" do
(_, spans) <- runInMemoryTracing do
inSpan "parent" Span.Kind.Internal mempty
. inSpan "child" Span.Kind.Internal mempty
$ do
addEventNow "event" mempty
pure ()
length spans `shouldBe` 2
let child = spans !! 0
fmap (.name) child.events `shouldBe` ["event"]
describe "inErrorSpan" do
prop "exports a span" \(Marker name) spanKind a -> do
s <-
only . snd
=<< ( runInMemoryTracing
. runErrorNoCallStack @AssertionFailed
. inErrorSpan @AssertionFailed @_ @_ name spanKind a
. pure
)
()
s.name `shouldBe` name
s.kind `shouldBe` spanKind
s.attributes `shouldBe` a
s.endTime `shouldSatisfy` (>= Just s.startTime)
it "no parent, unique ids" do
(_, spans) <- runInMemoryTracing . runErrorNoCallStack @AssertionFailed $ do
inErrorSpan @AssertionFailed @_ @() "a" Span.Kind.Internal mempty $ pure ()
inErrorSpan @AssertionFailed @_ @() "b" Span.Kind.Internal mempty $ pure ()
inErrorSpan @AssertionFailed @_ @() "c" Span.Kind.Internal mempty $ pure ()
mapM_ ((`shouldBe` Nothing) . (.parentSpanId)) spans
(spans <&> (.context.spanId)) `shouldSatisfy` allUnique
describe "nesting" do
it "parent-child chain" do
(_, spans) <- runInMemoryTracing . runErrorNoCallStack @AssertionFailed $ do
inErrorSpan @AssertionFailed @_ @() "root" Span.Kind.Internal mempty
. inErrorSpan @AssertionFailed @_ @() "middle" Span.Kind.Internal mempty
. inErrorSpan @AssertionFailed @_ @() "leaf" Span.Kind.Internal mempty
$ pure ()
length spans `shouldBe` 3
let leaf = spans !! 0
middle = spans !! 1
root = spans !! 2
leaf.parentSpanId `shouldBe` Just middle.context.spanId
middle.parentSpanId `shouldBe` Just root.context.spanId
root.parentSpanId `shouldBe` Nothing
it "shared parent" do
(_, spans) <- runInMemoryTracing . runErrorNoCallStack @AssertionFailed $ do
inErrorSpan @AssertionFailed @_ @() "parent" Span.Kind.Internal mempty do
inErrorSpan @AssertionFailed @_ @() "child-a" Span.Kind.Internal mempty $ pure ()
inErrorSpan @AssertionFailed @_ @() "child-b" Span.Kind.Internal mempty $ pure ()
length spans `shouldBe` 3
let childA = spans !! 0
childB = spans !! 1
parent = spans !! 2
childA.parentSpanId `shouldBe` Just parent.context.spanId
childB.parentSpanId `shouldBe` Just parent.context.spanId
it "restores parent context" do
(ctx, _) <- runInMemoryTracing . runErrorNoCallStack @AssertionFailed $ do
inErrorSpan @AssertionFailed @_ @_ "outer" Span.Kind.Internal mempty do
inErrorSpan @AssertionFailed @_ @() "inner" Span.Kind.Internal mempty $ pure ()
currentContext
ctx `shouldSatisfy` \case
Right (Just _) -> True
_ -> False
describe "concurrency" do
it "no loss, unique ids" do
let n = 500 :: Int
(_, spans) <-
runInMemoryTracing
. runErrorNoCallStack @AssertionFailed
. runConcurrent
. replicateConcurrently_ n
. inErrorSpan @AssertionFailed @_ @() "parallel" Span.Kind.Internal mempty
$ pure ()
length spans `shouldBe` n
(spans <&> (.context.spanId)) `shouldSatisfy` allUnique
it "concurrent nesting" do
(_, spans) <-
runInMemoryTracing
. runErrorNoCallStack @AssertionFailed
. runConcurrent
. inErrorSpan @AssertionFailed @_ @() "root" Span.Kind.Internal mempty
$ concurrently_
(inSpan "branch-a" Span.Kind.Internal mempty $ pure ())
(inSpan "branch-b" Span.Kind.Internal mempty $ pure ())
length spans `shouldBe` 3
case filter (("root" ==) . (.name)) spans of
[root] -> do
let branches = filter ((/= "root") . (.name)) spans
mapM_ ((`shouldBe` Just root.context.spanId) . (.parentSpanId)) branches
_ -> expectationFailure "expected exactly one root span"
describe "exception handling" do
prop "Error status on exception" \(Marker msg) -> do
(result, spans) <-
runInMemoryTracing
. runErrorNoCallStack @AssertionFailed
. inErrorSpan
@AssertionFailed
@_
@()
"item"
Span.Kind.Internal
mempty
. throwError
$ AssertionFailed (Text.unpack msg)
result `shouldSatisfy` isLeft
length spans `shouldBe` 1
forM_ spans \s -> do
s.status.code `shouldBe` Span.Status.Error
s.status.message `shouldBe` msg
length s.events `shouldBe` 1
fmap (.name) s.events `shouldBe` ["exception"]
it "Ok status without exception" do
(_, spans) <-
runInMemoryTracing
. runErrorNoCallStack @AssertionFailed
. inErrorSpan
@AssertionFailed
@_
@()
"item"
Span.Kind.Internal
mempty
$ pure ()
length spans `shouldBe` 1
forM_ (spans <&> (.status)) \s -> do
s.code `shouldBe` Span.Status.Unset
s.message `shouldBe` mempty
describe "events" do
prop "records events in order" \(Marker n1) a1 (Marker n2) a2 -> do
s <-
only . snd =<< runInMemoryTracing do
runErrorNoCallStack @AssertionFailed
. inErrorSpan @AssertionFailed @_ @() "test-span" Span.Kind.Internal mempty
$ do
addEventNow n1 a1
addEventNow n2 a2
pure ()
fmap (\e -> (e.name, e.attributes)) s.events `shouldBe` [(n1, a1), (n2, a2)]
it "events go to innermost span" do
(_, spans) <-
runInMemoryTracing
. runErrorNoCallStack @AssertionFailed
. inErrorSpan @AssertionFailed @_ @() "parent" Span.Kind.Internal mempty
. inErrorSpan @AssertionFailed @_ @() "child" Span.Kind.Internal mempty
$ do
addEventNow "event" mempty
pure ()
length spans `shouldBe` 2
let child = spans !! 0
fmap (.name) child.events `shouldBe` ["event"]
describe "inOkSpan" do
prop "exports a span" \(Marker name) spanKind a -> do
s <- only . snd =<< (runInMemoryTracing . inOkSpan name spanKind a . pure) ()
s.name `shouldBe` name
s.kind `shouldBe` spanKind
s.attributes `shouldBe` a
s.endTime `shouldSatisfy` (>= Just s.startTime)
it "no parent, unique ids" do
(_, spans) <- runInMemoryTracing do
inOkSpan "a" Span.Kind.Internal mempty $ pure ()
inOkSpan "b" Span.Kind.Internal mempty $ pure ()
inOkSpan "c" Span.Kind.Internal mempty $ pure ()
mapM_ ((`shouldBe` Nothing) . (.parentSpanId)) spans
(spans <&> (.context.spanId)) `shouldSatisfy` allUnique
describe "nesting" do
it "parent-child chain" do
(_, spans) <- runInMemoryTracing do
inOkSpan "root" Span.Kind.Internal mempty
. inOkSpan "middle" Span.Kind.Internal mempty
. inOkSpan "leaf" Span.Kind.Internal mempty
$ pure ()
length spans `shouldBe` 3
let leaf = spans !! 0
middle = spans !! 1
root = spans !! 2
leaf.parentSpanId `shouldBe` Just middle.context.spanId
middle.parentSpanId `shouldBe` Just root.context.spanId
root.parentSpanId `shouldBe` Nothing
it "shared parent" do
(_, spans) <- runInMemoryTracing do
inOkSpan "parent" Span.Kind.Internal mempty do
inOkSpan "child-a" Span.Kind.Internal mempty $ pure ()
inOkSpan "child-b" Span.Kind.Internal mempty $ pure ()
length spans `shouldBe` 3
let childA = spans !! 0
childB = spans !! 1
parent = spans !! 2
childA.parentSpanId `shouldBe` Just parent.context.spanId
childB.parentSpanId `shouldBe` Just parent.context.spanId
it "restores parent context" do
(ctx, _) <- runInMemoryTracing do
inOkSpan "outer" Span.Kind.Internal mempty do
inOkSpan "inner" Span.Kind.Internal mempty $ pure ()
currentContext
ctx `shouldSatisfy` \case
Just _ -> True
Nothing -> False
describe "concurrency" do
it "no loss, unique ids" do
let n = 500 :: Int
(_, spans) <-
runInMemoryTracing
. runConcurrent
. replicateConcurrently_ n
. inOkSpan "parallel" Span.Kind.Internal mempty
$ pure ()
length spans `shouldBe` n
(spans <&> (.context.spanId)) `shouldSatisfy` allUnique
it "concurrent nesting" do
(_, spans) <-
runInMemoryTracing
. runConcurrent
. inOkSpan "root" Span.Kind.Internal mempty
$ concurrently_
(inOkSpan "branch-a" Span.Kind.Internal mempty $ pure ())
(inOkSpan "branch-b" Span.Kind.Internal mempty $ pure ())
length spans `shouldBe` 3
case filter (("root" ==) . (.name)) spans of
[root] -> do
let branches = filter ((/= "root") . (.name)) spans
mapM_ ((`shouldBe` Just root.context.spanId) . (.parentSpanId)) branches
_ -> expectationFailure "expected exactly one root span"
describe "exception handling" do
it "no span on exception" do
(_, spans) <-
runInMemoryTracing
. try @AssertionFailed
. inOkSpan "item" Span.Kind.Internal mempty
. throwIO
$ AssertionFailed "bla"
length spans `shouldBe` 0
it "Ok status without exception" do
(_, spans) <-
runInMemoryTracing . inOkSpan "item" Span.Kind.Internal mempty $
pure ()
length spans `shouldBe` 1
forM_ (spans <&> (.status)) \s -> do
s.code `shouldBe` Span.Status.Ok
s.message `shouldBe` mempty
describe "events" do
prop "records events in order" \(Marker n1) a1 (Marker n2) a2 -> do
s <-
only . snd =<< runInMemoryTracing do
inOkSpan "test-span" Span.Kind.Internal mempty do
addEventNow n1 a1
addEventNow n2 a2
pure ()
fmap (\e -> (e.name, e.attributes)) s.events `shouldBe` [(n1, a1), (n2, a2)]
it "events go to innermost span" do
(_, spans) <- runInMemoryTracing do
inSpan "parent" Span.Kind.Internal mempty
. inOkSpan "child" Span.Kind.Internal mempty
$ do
addEventNow "event" mempty
pure ()
length spans `shouldBe` 2
let child = spans !! 0
fmap (.name) child.events `shouldBe` ["event"]