packages feed

freckle-app-1.18.1.0: library/Freckle/App/OpenTelemetry/Context.hs

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

import Freckle.App.Prelude

import Control.Error.Util (hush)
import Control.Lens (Lens', lens, (&), (.~), (^.))
import Control.Monad.Catch (MonadMask)
import Data.Aeson (FromJSON, ToJSON)
import Data.ByteString (ByteString)
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive qualified as CI
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 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, extract, inject)
import OpenTelemetry.Trace.Core
  ( getTracerProviderPropagators
  , getTracerTracerProvider
  )

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 (Semigroup, Monoid)
  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 . encodeUtf8) encodeUtf8

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

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

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

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

-- | Process an item (a request, a Job, etc) in a top-level span and context
processWithContext
  :: ( MonadUnliftIO m
     , MonadMask m
     , MonadTracer m
     , HasHeaders a
     , HasCallStack
     )
  => 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'