e11y-otel-0.1.0.0: test/Test.hs
-- Copyright 2024 Shea Levy
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UnicodeSyntax #-}
module Main where
import Control.Concurrent.Async
import Control.Concurrent.MVar
import Control.Exception
import Control.Monad.With
import Data.Foldable
import Data.HashMap.Strict hiding (toList)
import Data.IORef
import Data.Kind
import Data.Maybe
import Data.Text qualified as T
import Observe.Event
import Observe.Event.Backend
import Observe.Event.Backend.OpenTelemetry
import OpenTelemetry.Attributes
import OpenTelemetry.Context
import OpenTelemetry.Context.ThreadLocal
import OpenTelemetry.Processor
import OpenTelemetry.Trace
import OpenTelemetry.Trace.Core
import OpenTelemetry.Util
import Test.Syd
data TestSelector ∷ Type → Type where
Test ∷ TestSelector TestField
data TestField = TestField deriving (Eq, Show)
type instance SubSelector TestField = SubTestSelector
data SubTestSelector ∷ Type → Type where
SubTest ∷ SubTestSelector SubTestField
data SubTestField = SubTestField deriving (Eq, Show)
type instance SubSelector SubTestField = NoEventsSelector
renderTestSelector ∷ RenderOTel TestSelector
renderTestSelector = selectorRendering $ \Test →
SelectorRendering
{ renderTopSelector =
OTelRendered
{ eventName = "test"
, eventKind = Internal
, renderField = \TestField → singleton "test-field" (AttributeValue $ BoolAttribute True)
}
, renderSubSelector = renderSubTestSelector
}
renderSubTestSelector ∷ RenderOTel SubTestSelector
renderSubTestSelector = selectorRendering $ \SubTest →
noSubEventsSelectorRendering $
OTelRendered
{ eventName = "sub-test"
, eventKind = Internal
, renderField = \SubTestField → singleton "sub-test-field" (AttributeValue $ BoolAttribute True)
}
data TestException = TestException deriving (Show)
instance Exception TestException
dummyTracer ∷ MVar [ImmutableSpan] → IO Tracer
dummyTracer var = do
(_, options) ← getTracerProviderInitializationOptions
let processor =
Processor
{ processorOnStart = \_ _ → pure ()
, processorOnEnd = \spanRef → do
s ← readIORef spanRef
modifyMVar_ var $ \spans → pure (s : spans)
, processorShutdown = async $ pure ShutdownSuccess
, processorForceFlush = pure ()
}
provider ← createTracerProvider [processor] options
pure $ makeTracer provider (InstrumentationLibrary "dummy" "1.0.0") tracerOptions
main ∷ IO ()
main = sydTest $ do
let setupTracer = do
spansVar ← newMVar []
tracer ← dummyTracer spansVar
pure (spansVar, tracer)
before setupTracer $ describe "TracerEventBackend" $ do
it "creates spans" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
(withEvent Test $ pure ()) ∷ IO ()
spans ← readMVar spansVar
s ← case spans of
[s] → pure s
_ → expectationFailure "wrong number of spans"
shouldSatisfyNamed
s
"has the expected name"
(\s' → s'.spanName == "test")
it "creates parentless events when the Context is empty" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
(withEvent Test $ pure ()) ∷ IO ()
spans ← readMVar spansVar
s ← case spans of
[] → expectationFailure "no spans created"
[s] → pure s
_ → expectationFailure "too many spans created"
shouldSatisfyNamed
s
"has no parent"
(\s' → isNothing s'.spanParent)
it "gets the parent from the Context when events are parentless" $ \(spansVar, tracer) → do
let be = TracerEventBackend{tracer, render = renderTestSelector}
let ?e11yBackend = be
_ ∷ () ← withEvent Test $ do
adjustContext $ insertSpan eventReference
let ?e11yBackend = be
(withEvent Test $ pure ()) ∷ IO ()
spans ← readMVar spansVar
(parent, child) ← case spans of
[parent, child] → pure (parent, child)
_ → expectationFailure "wrong number of spans"
childParentContext ← traverse getSpanContext child.spanParent
case childParentContext of
Nothing → expectationFailure "child has no parent"
Just ctx → shouldSatisfyNamed ctx "parent is the right parent" (== parent.spanContext)
it "ignores the Context when an event has a parent" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
(s1 ∷ Span) ← withEvent Test $ pure eventReference
_ ∷ () ← withEvent Test $ do
adjustContext $ insertSpan s1
(withEvent SubTest $ pure ()) ∷ IO ()
spans ← readMVar spansVar
(parent, child) ← case spans of
[parent, child, _] → pure (parent, child)
_ → expectationFailure "wrong number of spans"
childParentContext ← traverse getSpanContext child.spanParent
case childParentContext of
Nothing → expectationFailure "child has no parent"
Just ctx → shouldSatisfyNamed ctx "parent is the right parent" (== parent.spanContext)
it "adds causes as links" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
_ ∷ () ← withEvent Test $ do
withRelatedEvent SubTest Nothing [eventReference] $ do pure ()
spans ← readMVar spansVar
(parent, child) ← case spans of
[parent, child] → pure (parent, child)
_ → expectationFailure "wrong number of spans"
childLink ← case toList $ frozenBoundedCollectionValues child.spanLinks of
[childLink] → pure childLink
_ → expectationFailure "wrong number of links"
shouldSatisfyNamed
childLink
"links to parent"
(\cl → cl.frozenLinkContext == parent.spanContext)
it "adds initial fields" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
let alloc =
allocateEventArgs $
EventParams
{ selectors = Leaf Test
, parent = Nothing
, causes = []
, initialFields = [TestField]
}
generalWith alloc $ \_ → pure ()
spans ← readMVar spansVar
s ← case spans of
[s] → pure s
_ → expectationFailure "wrong number of spans"
let checkAttrs s' = (snd . getAttributes $ s'.spanAttributes) ! "test-field" == AttributeValue (BoolAttribute True)
shouldSatisfyNamed
s
"has a test attr"
checkAttrs
it "creates instant parentless events as spans when the Context is empty" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
!_ ← instantEvent Test []
spans ← readMVar spansVar
s ← case spans of
[] → expectationFailure "no spans created"
[s] → pure s
_ → expectationFailure "too many spans created"
shouldSatisfyNamed
s
"has no parent"
(\s' → isNothing s'.spanParent)
it "creates instant parentless events as span events of the Context's span when it has one" $ \(spansVar, tracer) → do
let be = TracerEventBackend{tracer, render = renderTestSelector}
let ?e11yBackend = be
_ ∷ () ← withEvent Test $ do
adjustContext $ insertSpan eventReference
let ?e11yBackend = be
!_ ← instantEvent Test []
pure ()
spans ← readMVar spansVar
parent ← case spans of
[parent] → pure parent
_ → expectationFailure "wrong number of spans"
let evs = toList . appendOnlyBoundedCollectionValues $ parent.spanEvents
child ← case evs of
[child] → pure child
_ → expectationFailure "wrong number of span events"
shouldSatisfyNamed
child
"is a test event"
(\c → c.eventName == "test")
it "adds fields span events generated by parented instant events" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
_ ∷ () ← withEvent Test $ do
_ ← instantEvent SubTest [SubTestField]
pure ()
spans ← readMVar spansVar
parent ← case spans of
[parent] → pure parent
_ → expectationFailure "wrong number of spans"
let evs = toList . appendOnlyBoundedCollectionValues $ parent.spanEvents
child ← case evs of
[child] → pure child
_ → expectationFailure "wrong number of span events"
let checkAttrs e = (snd . getAttributes $ e.eventAttributes) ! "sub-test-field" == AttributeValue (BoolAttribute True)
shouldSatisfyNamed
child
"has a subtest attr"
checkAttrs
it "sets event exceptions" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
(withEvent Test $ finalizeEvent (Just (SomeException TestException))) ∷ IO ()
spans ← readMVar spansVar
s ← case spans of
[s] → pure s
_ → expectationFailure "wrong number of spans"
let evs = toList . appendOnlyBoundedCollectionValues $ s.spanEvents
checkAttrs e = (snd . getAttributes $ e.eventAttributes) ! "exception.escaped" == AttributeValue (BoolAttribute True)
errorEv ← case evs of
[errorEv] → pure errorEv
_ → expectationFailure "wrong number of span events"
shouldSatisfyNamed
(s, errorEv)
"has exception information"
(\(s', errorEv') → s'.spanStatus == Error (T.pack $ show TestException) && checkAttrs errorEv')
it "adds fields as attributes" $ \(spansVar, tracer) → do
let ?e11yBackend = TracerEventBackend{tracer, render = renderTestSelector}
(withEvent Test $ addEventField TestField) ∷ IO ()
spans ← readMVar spansVar
s ← case spans of
[s] → pure s
_ → expectationFailure "wrong number of spans"
let checkAttrs s' = (snd . getAttributes $ s'.spanAttributes) ! "test-field" == AttributeValue (BoolAttribute True)
shouldSatisfyNamed
s
"has a test attr"
checkAttrs