packages feed

freckle-otel-0.1.0.0: library/Freckle/App/OpenTelemetry/Context.hs

module Freckle.App.OpenTelemetry.Context
  ( HasHeaders (..)
  , CustomTraceContext (..)
  , extractContext
  , injectContext
  , processWithContext
  ) where

import Prelude

import Control.Error.Util (hush)
import Control.Lens (Lens', lens, (.~), (^.))
import Control.Monad.Catch (MonadMask)
import Control.Monad.IO.Class (MonadIO)
import Data.Aeson (FromJSON, ToJSON)
import Data.Bifunctor (bimap)
import Data.ByteString (ByteString)
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive qualified as CI
import Data.Function ((&))
import Data.Functor (void)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Encoding qualified as T
import Faktory.Job (Job, custom, jobOptions)
import Faktory.Job.Custom (fromCustom, toCustom)
import Faktory.JobOptions (JobOptions (..))
import Freckle.App.OpenTelemetry
  ( MonadTracer (..)
  , SpanArguments
  , inSpan
  )
import Freckle.App.OpenTelemetry.ThreadContext (withTraceContext)
import GHC.Generics (Generic)
import GHC.Stack (HasCallStack)
import Network.HTTP.Client (Request, requestHeaders)
import Network.HTTP.Simple (setRequestHeaders)
import Network.HTTP.Types.Header (Header)
import OpenTelemetry.Context (Context)
import OpenTelemetry.Context.ThreadLocal (attachContext, getContext)
import OpenTelemetry.Propagator
  ( Propagator
  , TextMap
  , extract
  , inject
  , textMapFromList
  , textMapToList
  )
import OpenTelemetry.Trace.Core
  ( getTracerProviderPropagators
  , getTracerTracerProvider
  )
import UnliftIO (MonadUnliftIO)

class HasHeaders a where
  headersL :: Lens' a [Header]

instance HasHeaders [Header] where
  headersL = id

instance HasHeaders Request where
  headersL = lens requestHeaders $ \req hs -> setRequestHeaders hs req

instance HasHeaders (Job a) where
  headersL = optionsL . customTraceContextL . headersL

optionsL :: Lens' (Job a) JobOptions
optionsL = lens jobOptions $ \x y -> x {jobOptions = y}

customTraceContextL :: Lens' JobOptions CustomTraceContext
customTraceContextL = lens get set
 where
  get jo = fromMaybe mempty $ hush . fromCustom =<< joCustom jo
  set jo = (jo <>) . custom . toCustom

-- | A type that can be stored as the @custom@ field of a Faktory 'Job'
newtype CustomTraceContext = CustomTraceContext
  { traceHeaders :: [(Text, Text)]
  }
  deriving stock (Generic)
  deriving newtype (Monoid, Semigroup)
  deriving anyclass (FromJSON, ToJSON)

instance HasHeaders CustomTraceContext where
  headersL = lens (map encode . traceHeaders) $ \x y -> x {traceHeaders = map decode y}

encode :: (Text, Text) -> (CI ByteString, ByteString)
encode = bimap (CI.mk . T.encodeUtf8) T.encodeUtf8

decode :: (CI ByteString, ByteString) -> (Text, Text)
decode = bimap (T.decodeUtf8 . CI.original) T.decodeUtf8

-- | Update our trace context from that extracted from the given item's headers
extractContext
  :: (HasHeaders a, MonadIO m, MonadTracer m) => a -> m ()
extractContext a = do
  context <- getContext
  propagator <- getPropagator
  updatedContext <- extract propagator (headersToTextMap $ a ^. headersL) context
  void $ attachContext updatedContext

-- | Inject our trace context into the given item's headers
injectContext
  :: (HasHeaders a, MonadIO m, MonadTracer m) => a -> m a
injectContext a = do
  context <- getContext
  propagator <- getPropagator
  headers <-
    textMapToHeaders
      <$> inject propagator context (headersToTextMap $ a ^. headersL)
  pure $ a & headersL .~ headers

getPropagator :: MonadTracer m => m (Propagator Context TextMap TextMap)
getPropagator =
  getTracerProviderPropagators . getTracerTracerProvider <$> getTracer

headersToTextMap :: [Header] -> TextMap
headersToTextMap = textMapFromList . map decode

textMapToHeaders :: TextMap -> [Header]
textMapToHeaders = map encode . textMapToList

-- | Process an item (a request, a Job, etc) in a top-level span and context
processWithContext
  :: ( HasCallStack
     , HasHeaders a
     , MonadMask m
     , MonadTracer m
     , MonadUnliftIO m
     )
  => Text
  -- ^ Span name
  -> SpanArguments
  -> a
  -> (a -> m b)
  -> m b
processWithContext name args a f = do
  extractContext a
  inSpan name args $ do
    a' <- injectContext a
    withTraceContext $ f a'