mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 01:44:03 +03:00
342391f39d
This upgrades the version of Ormolu required by the HGE repository to v0.5.0.1, and reformats all code accordingly. Ormolu v0.5 reformats code that uses infix operators. This is mostly useful, adding newlines and indentation to make it clear which operators are applied first, but in some cases, it's unpleasant. To make this easier on the eyes, I had to do the following: * Add a few fixity declarations (search for `infix`) * Add parentheses to make precedence clear, allowing Ormolu to keep everything on one line * Rename `relevantEq` to `(==~)` in #6651 and set it to `infix 4` * Add a few _.ormolu_ files (thanks to @hallettj for helping me get started), mostly for Autodocodec operators that don't have explicit fixity declarations In general, I think these changes are quite reasonable. They mostly affect indentation. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6675 GitOrigin-RevId: cd47d87f1d089fb0bc9dcbbe7798dbceedcd7d83
307 lines
11 KiB
Haskell
307 lines
11 KiB
Haskell
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
module Hasura.Tracing
|
|
( MonadTrace (..),
|
|
TraceT,
|
|
runTraceT,
|
|
runTraceTWith,
|
|
runTraceTWithReporter,
|
|
runTraceTInContext,
|
|
interpTraceT,
|
|
TraceContext (..),
|
|
Reporter (..),
|
|
noReporter,
|
|
HasReporter (..),
|
|
TracingMetadata,
|
|
extractB3HttpContext,
|
|
tracedHttpRequest,
|
|
injectEventContext,
|
|
extractEventContext,
|
|
)
|
|
where
|
|
|
|
import Control.Lens (over, view, (^?))
|
|
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
|
|
import Control.Monad.Morph
|
|
import Control.Monad.Trans.Control
|
|
import Data.Aeson qualified as J
|
|
import Data.Aeson.Lens qualified as JL
|
|
import Data.ByteString.Char8 qualified as Char8
|
|
import Data.String (fromString)
|
|
import Hasura.Prelude
|
|
import Hasura.Tracing.TraceId
|
|
( SpanId,
|
|
TraceId,
|
|
randomSpanId,
|
|
randomTraceId,
|
|
spanIdFromHex,
|
|
spanIdToHex,
|
|
traceIdFromHex,
|
|
traceIdToHex,
|
|
)
|
|
import Network.HTTP.Client.Manager (HasHttpManagerM (..))
|
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
|
|
|
-- | Any additional human-readable key-value pairs relevant
|
|
-- to the execution of a block of code.
|
|
type TracingMetadata = [(Text, Text)]
|
|
|
|
newtype Reporter = Reporter
|
|
{ runReporter ::
|
|
forall io a.
|
|
MonadIO io =>
|
|
TraceContext ->
|
|
-- the current trace context
|
|
Text ->
|
|
-- human-readable name for this block of code
|
|
io (a, TracingMetadata) ->
|
|
-- the action whose execution we want to report, returning
|
|
-- any metadata emitted
|
|
io a
|
|
}
|
|
|
|
noReporter :: Reporter
|
|
noReporter = Reporter \_ _ -> fmap fst
|
|
|
|
-- | A type class for monads which support some way to report execution traces.
|
|
--
|
|
-- See @instance Tracing.HasReporter (AppM impl)@ in @HasuraPro.App@.
|
|
class Monad m => HasReporter m where
|
|
-- | Get the current tracer
|
|
askReporter :: m Reporter
|
|
default askReporter :: m Reporter
|
|
askReporter = pure noReporter
|
|
|
|
instance HasReporter m => HasReporter (ReaderT r m) where
|
|
askReporter = lift askReporter
|
|
|
|
instance HasReporter m => HasReporter (ExceptT e m) where
|
|
askReporter = lift askReporter
|
|
|
|
-- | A trace context records the current active trace,
|
|
-- the active span within that trace, and the span's parent,
|
|
-- unless the current span is the root.
|
|
data TraceContext = TraceContext
|
|
{ -- | TODO what is this exactly? The topmost span id?
|
|
tcCurrentTrace :: !TraceId,
|
|
tcCurrentSpan :: !SpanId,
|
|
tcCurrentParent :: !(Maybe SpanId)
|
|
}
|
|
|
|
-- | The 'TraceT' monad transformer adds the ability to keep track of
|
|
-- the current trace context.
|
|
newtype TraceT m a = TraceT {unTraceT :: ReaderT (TraceContext, Reporter) (WriterT TracingMetadata m) a}
|
|
deriving (Functor, Applicative, Monad, MonadIO, MonadFix, MonadMask, MonadCatch, MonadThrow, MonadBase b, MonadBaseControl b)
|
|
|
|
instance MonadTrans TraceT where
|
|
lift = TraceT . lift . lift
|
|
|
|
instance MFunctor TraceT where
|
|
hoist f (TraceT rwma) = TraceT (hoist (hoist f) rwma)
|
|
|
|
instance MonadError e m => MonadError e (TraceT m) where
|
|
throwError = lift . throwError
|
|
catchError (TraceT m) f = TraceT (catchError m (unTraceT . f))
|
|
|
|
instance MonadReader r m => MonadReader r (TraceT m) where
|
|
ask = TraceT $ lift ask
|
|
local f m = TraceT $ mapReaderT (local f) (unTraceT m)
|
|
|
|
instance (HasHttpManagerM m) => HasHttpManagerM (TraceT m) where
|
|
askHttpManager = lift askHttpManager
|
|
|
|
-- | Run an action in the 'TraceT' monad transformer.
|
|
-- 'runTraceT' delimits a new trace with its root span, and the arguments
|
|
-- specify a name and metadata for that span.
|
|
runTraceT :: (HasReporter m, MonadIO m) => Text -> TraceT m a -> m a
|
|
runTraceT name tma = do
|
|
rep <- askReporter
|
|
runTraceTWithReporter rep name tma
|
|
|
|
runTraceTWith :: MonadIO m => TraceContext -> Reporter -> Text -> TraceT m a -> m a
|
|
runTraceTWith ctx rep name tma =
|
|
runReporter rep ctx name $
|
|
runWriterT $
|
|
runReaderT (unTraceT tma) (ctx, rep)
|
|
|
|
-- | Run an action in the 'TraceT' monad transformer in an
|
|
-- existing context.
|
|
runTraceTInContext :: (MonadIO m, HasReporter m) => TraceContext -> Text -> TraceT m a -> m a
|
|
runTraceTInContext ctx name tma = do
|
|
rep <- askReporter
|
|
runTraceTWith ctx rep name tma
|
|
|
|
-- | Run an action in the 'TraceT' monad transformer in an
|
|
-- existing context.
|
|
runTraceTWithReporter :: MonadIO m => Reporter -> Text -> TraceT m a -> m a
|
|
runTraceTWithReporter rep name tma = do
|
|
ctx <-
|
|
TraceContext
|
|
<$> liftIO randomTraceId
|
|
<*> liftIO randomSpanId
|
|
<*> pure Nothing
|
|
runTraceTWith ctx rep name tma
|
|
|
|
-- | Monads which support tracing. 'TraceT' is the standard example.
|
|
class Monad m => MonadTrace m where
|
|
-- | Trace the execution of a block of code, attaching a human-readable name.
|
|
trace :: Text -> m a -> m a
|
|
|
|
-- | Ask for the current tracing context, so that we can provide it to any
|
|
-- downstream services, e.g. in HTTP headers.
|
|
currentContext :: m TraceContext
|
|
|
|
-- | Ask for the current tracing reporter
|
|
currentReporter :: m Reporter
|
|
|
|
-- | Log some metadata to be attached to the current span
|
|
attachMetadata :: TracingMetadata -> m ()
|
|
|
|
-- | Reinterpret a 'TraceT' action in another 'MonadTrace'.
|
|
-- This can be useful when you need to reorganize a monad transformer stack, for
|
|
-- example, to embed an action in some monadic computation, while preserving tracing
|
|
-- metadata and context.
|
|
--
|
|
-- For example, we use this function in various places in 'BackendExecute',
|
|
-- where we receive an action to execute in some concrete monad transformer stack.
|
|
-- See the various implementations of 'runQuery' for examples.
|
|
-- Ideally, the input computation's type would be sufficiently polymorphic that
|
|
-- we would not need to reorder monads inthe transformer stack. However, the monad
|
|
-- transformer stacks must be concrete, because their types are defined by
|
|
-- an associated type family 'ExecutionMonad'. Hence, we need to use this function
|
|
-- to peel off the outermost 'TraceT' constructor, and embed the computation in some
|
|
-- other 'MonadTrace'.
|
|
--
|
|
-- A second example is related to caching. The 'cacheLookup' function returns an
|
|
-- action in a concrete transformer stack, again because we are constrained by the
|
|
-- usage of a type class. We need to reinterpret the 'TraceT' component of this
|
|
-- concrete stack in some other abstract monad transformer stack, using this function.
|
|
--
|
|
-- Laws:
|
|
--
|
|
-- > interpTraceT id (hoist f (TraceT x)) = interpTraceT f (TraceT x)
|
|
interpTraceT ::
|
|
MonadTrace n =>
|
|
(m (a, TracingMetadata) -> n (b, TracingMetadata)) ->
|
|
TraceT m a ->
|
|
n b
|
|
interpTraceT f (TraceT rwma) = do
|
|
ctx <- currentContext
|
|
rep <- currentReporter
|
|
(b, meta) <- f (runWriterT (runReaderT rwma (ctx, rep)))
|
|
attachMetadata meta
|
|
pure b
|
|
|
|
-- | If the underlying monad can report trace data, then 'TraceT' will
|
|
-- collect it and hand it off to that reporter.
|
|
instance MonadIO m => MonadTrace (TraceT m) where
|
|
-- Note: this implementation is so awkward because we don't want to give the
|
|
-- derived MonadReader/Writer instances to TraceT
|
|
trace name ma = TraceT . ReaderT $ \(ctx, rep) -> do
|
|
spanId <- liftIO randomSpanId
|
|
let subCtx =
|
|
ctx
|
|
{ tcCurrentSpan = spanId,
|
|
tcCurrentParent = Just (tcCurrentSpan ctx)
|
|
}
|
|
lift . runReporter rep subCtx name . runWriterT $ runReaderT (unTraceT ma) (subCtx, rep)
|
|
|
|
currentContext = TraceT (asks fst)
|
|
|
|
currentReporter = TraceT (asks snd)
|
|
|
|
attachMetadata = TraceT . tell
|
|
|
|
instance MonadTrace m => MonadTrace (ReaderT r m) where
|
|
trace = mapReaderT . trace
|
|
currentContext = lift currentContext
|
|
currentReporter = lift currentReporter
|
|
attachMetadata = lift . attachMetadata
|
|
|
|
instance MonadTrace m => MonadTrace (StateT e m) where
|
|
trace = mapStateT . trace
|
|
currentContext = lift currentContext
|
|
currentReporter = lift currentReporter
|
|
attachMetadata = lift . attachMetadata
|
|
|
|
instance MonadTrace m => MonadTrace (ExceptT e m) where
|
|
trace = mapExceptT . trace
|
|
currentContext = lift currentContext
|
|
currentReporter = lift currentReporter
|
|
attachMetadata = lift . attachMetadata
|
|
|
|
-- | Inject the trace context as a set of HTTP headers.
|
|
injectB3HttpContext :: TraceContext -> [HTTP.Header]
|
|
injectB3HttpContext TraceContext {..} =
|
|
("X-B3-TraceId", traceIdToHex tcCurrentTrace)
|
|
: ("X-B3-SpanId", spanIdToHex tcCurrentSpan)
|
|
: [ ("X-B3-ParentSpanId", spanIdToHex parentID)
|
|
| parentID <- maybeToList tcCurrentParent
|
|
]
|
|
|
|
-- | Extract the trace and parent span headers from a HTTP request
|
|
-- and create a new 'TraceContext'. The new context will contain
|
|
-- a fresh span ID, and the provided span ID will be assigned as
|
|
-- the immediate parent span.
|
|
extractB3HttpContext :: [HTTP.Header] -> IO (Maybe TraceContext)
|
|
extractB3HttpContext hdrs = do
|
|
freshSpanId <- liftIO randomSpanId
|
|
-- B3 TraceIds can have a length of either 64 bits (16 hex chars) or 128 bits
|
|
-- (32 hex chars). For 64-bit TraceIds, we pad them with zeros on the left to
|
|
-- make them 128 bits long.
|
|
let traceId =
|
|
lookup "X-B3-TraceId" hdrs >>= \rawTraceId ->
|
|
if
|
|
| Char8.length rawTraceId == 32 ->
|
|
traceIdFromHex rawTraceId
|
|
| Char8.length rawTraceId == 16 ->
|
|
traceIdFromHex $ Char8.replicate 16 '0' <> rawTraceId
|
|
| otherwise ->
|
|
Nothing
|
|
pure $
|
|
TraceContext
|
|
<$> traceId
|
|
<*> pure freshSpanId
|
|
<*> pure (spanIdFromHex =<< lookup "X-B3-SpanId" hdrs)
|
|
|
|
-- | Inject the trace context as a JSON value, appropriate for
|
|
-- storing in (e.g.) an event trigger payload.
|
|
injectEventContext :: TraceContext -> J.Value
|
|
injectEventContext TraceContext {..} =
|
|
J.object
|
|
[ "trace_id" J..= bsToTxt (traceIdToHex tcCurrentTrace),
|
|
"span_id" J..= bsToTxt (spanIdToHex tcCurrentSpan)
|
|
]
|
|
|
|
-- | Extract a trace context from an event trigger payload.
|
|
extractEventContext :: J.Value -> IO (Maybe TraceContext)
|
|
extractEventContext e = do
|
|
freshSpanId <- randomSpanId
|
|
pure $
|
|
TraceContext
|
|
<$> (traceIdFromHex . txtToBs =<< e ^? JL.key "trace_context" . JL.key "trace_id" . JL._String)
|
|
<*> pure freshSpanId
|
|
<*> pure (spanIdFromHex . txtToBs =<< e ^? JL.key "trace_context" . JL.key "span_id" . JL._String)
|
|
|
|
-- | Perform HTTP request which supports Trace headers using a
|
|
-- HTTP.Request value
|
|
--
|
|
-- TODO REFACTOR:
|
|
-- - inline 'HTTP.performRequest' so that we can be sure a trace is always logged
|
|
-- - Inline 'try' here since we always use that at call sites
|
|
tracedHttpRequest ::
|
|
MonadTrace m =>
|
|
-- | http request that needs to be made
|
|
HTTP.Request ->
|
|
-- | a function that takes the traced request and executes it
|
|
(HTTP.Request -> m a) ->
|
|
m a
|
|
tracedHttpRequest req f = do
|
|
let method = bsToTxt (view HTTP.method req)
|
|
uri = view HTTP.url req
|
|
trace (method <> " " <> uri) do
|
|
let reqBytes = HTTP.getReqSize req
|
|
attachMetadata [("request_body_bytes", fromString (show reqBytes))]
|
|
ctx <- currentContext
|
|
f $ over HTTP.headers (injectB3HttpContext ctx <>) req
|