packages feed

eventuo11y-otel-0.1.0.0: src/Observe/Event/Render/OpenTelemetry.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}

-- |
-- Description : EventBackend for rendering events as OpenTelemetry traces
-- Copyright   : Copyright 2023 Shea Levy.
-- License     : Apache-2.0
-- Maintainer  : shea@shealevy.com
module Observe.Event.Render.OpenTelemetry where

import Control.Monad.IO.Class
import Data.Text (Text, pack)
import Observe.Event.Backend
import OpenTelemetry.Context
import OpenTelemetry.Context.ThreadLocal
import OpenTelemetry.Trace.Core hiding (Event)
import OpenTelemetry.Trace.Id

-- | An 'EventBackend' built on a 'Tracer'.
--
-- When no explicit parent is set, the backend will try to find a parent in the "OpenTelemetry.Context.ThreadLocal" 'Context'.
-- However, it will never update that 'Context', as the primitive 'EventBackend' API has no way to determine if it's being
-- consumed in a scoped context or one allowing for general interleaving.
--
-- When possible, events created with 'emitImmediateEvent' will use the span event API. However, this requires a parent event
-- (explicitly specified or found in the thread-local 'Context'), so the backend will fallback to creating and 'finalize'ing a new
-- 'Span'. If a span event is created, the resulting 'reference' will be to its parent, as span events cannot be parents/links. Span
-- events do not allow for non-parent links, so any `newEventCauses` are dropped; in the future, we may either add them as custom
-- 'Attribute's or fall back to a full span if any are specified.
--
-- Event 'Link's are currently not given any attributes. In the future, arbitrary link metadata could be added to the core 'EventBackend'
-- API, in which case we could add a renderer for the link metadata type.
--
-- Currently the backend lets the underlying 'Tracer' set all timestamps. In the future, 'RenderSelectorOTel' could be allowed to run in
-- @m@ and have a timestamp field.
--
-- Exceptions passed to 'finalize' are 'recordException'ed without any custom attributes. In the future, an @Exception -> [Text, Attribute]@
-- argument could be added, or arbitrary exception metadata added to 'finalize'.
tracerEventBackend :: (MonadIO m) => Tracer -> RenderSelectorOTel s -> EventBackend m Span s
tracerEventBackend tracer render = backend
  where
    backend =
      EventBackend
        { newEvent = \args@(NewEventArgs {..}) -> do
            ctx <- maybe empty id <$> lookupContext
            let ctx' = case newEventParent of
                  Just s -> insertSpan s ctx
                  Nothing -> ctx
                OTelRendered {..} = render newEventSelector
            links <- traverse (fmap (flip NewLink []) . getSpanContext) newEventCauses
            s <-
              createSpanWithoutCallStack tracer ctx' eventName $
                SpanArguments
                  { kind = eventKind,
                    attributes = concatMap renderField newEventInitialFields,
                    links = links,
                    startTime = Nothing
                  }
            pure $
              Event
                { reference = s,
                  addField = addAttributes s . renderField,
                  finalize = \me -> do
                    let recordError e = do
                          recordException s [("exception.escaped", toAttribute True)] Nothing e
                          setStatus s . Error . pack $ show e
                    maybe (setStatus s Ok) recordError me
                    endSpan s Nothing
                },
          emitImmediateEvent = \args@(NewEventArgs {..}) -> case newEventParent of
            Nothing -> do
              m_ctx <- lookupContext
              case m_ctx >>= lookupSpan of
                Just s ->
                  emitImmediateEvent backend $
                    args
                      { newEventParent = Just s
                      }
                Nothing -> do
                  ev <- newEvent backend args
                  finalize ev Nothing
                  pure $ reference ev
            Just s -> do
              let OTelRendered {..} = render newEventSelector
              addEvent s $
                NewEvent
                  { newEventName = eventName,
                    newEventAttributes = concatMap renderField newEventInitialFields,
                    newEventTimestamp = Nothing
                  }
              pure s
        }

-- | Render a given selector (and all of its fields) to OpenTelemetry
type RenderSelectorOTel s = forall f. s f -> OTelRendered f

-- | The result of rendering a specific selector with field type @f@
data OTelRendered f = OTelRendered
  { -- | The name of the event. See section on "span name" at <https://opentelemetry.io/docs/reference/specification/trace/api/#span>
    eventName :: !Text,
    -- | See the specification on [SpanKind](https://opentelemetry.io/docs/reference/specification/trace/api/#spankind)
    eventKind :: !SpanKind,
    -- | Render a field to a set of span [attributes](https://opentelemetry.io/docs/reference/specification/common/#attribute).
    --
    -- Note especially the [attribute naming guidelines](https://opentelemetry.io/docs/reference/specification/common/attribute-naming/)
    renderField :: !(f -> [(Text, Attribute)])
  }