effectful-tracing-0.1.0.0: test/Effectful/Tracing/CompileTest.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Module : Effectful.Tracing.CompileTest
-- Description : Compile-only checks that the public API and the documented
-- examples still typecheck.
--
-- Two compile-only checks, both of which turn the test suite red if the public
-- surface stops typechecking. Neither runs.
--
-- * @smart-constructor API typechecks@ pins the core emit/lifecycle surface to
-- a concrete effect stack.
-- * @documentation examples typecheck@ mirrors the code blocks in @README.md@,
-- @docs/tutorial.md@, and @docs/cookbook.md@. Those blocks are deliberately
-- pedagogical fragments (undefined placeholder names, scattered imports, bare
-- expressions), so they cannot be compiled in place by cabal-docspec or
-- markdown-unlit. Instead each is reproduced here against the real API: a
-- renamed export or changed signature breaks this module, flagging the docs
-- as stale. The placeholder types and effects are stubbed once below. This
-- checks the API shapes the docs use, not the literal doc bytes, so prose
-- edits that change a call still need the matching mirror updated here.
module Effectful.Tracing.CompileTest
( tests
) where
import Control.Exception (ErrorCall (ErrorCall), toException)
import Data.Text (Text)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, IOE, runEff, (:>))
import Effectful.Concurrent (Concurrent)
import Effectful.Dispatch.Dynamic (send)
import Network.HTTP.Types (Header)
import System.IO (hIsTerminalDevice, stderr)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Effectful.Tracing
( Span
, SpanArguments (attributes, kind)
, SpanContext
, SpanKind (Client)
, SpanStatus (Error, Ok)
, Tracer
, addAttribute
, addAttributes
, addEvent
, alwaysOn
, defaultParentBasedConfig
, defaultSpanArguments
, extractContext
, getActiveSpan
, parentBased
, recordException
, setStatus
, traceIdRatioBased
, withRemoteParent
, withSpan
, withSpan'
, (.=)
)
import Effectful.Tracing.Attribute (Attribute (Attribute), AttributeValue (AttrBool, AttrInt))
import Effectful.Tracing.EnvConfig
( EnvConfig (resourceAttributes, serviceName, traceContextPropagators, tracesSampler)
, readEnvConfig
)
import Effectful.Tracing.Baggage
( BaggageContext
, getBaggage
, lookupBaggageValue
, runBaggageWith
, withBaggageEntry
)
import Control.Monad.IO.Class (liftIO)
import Effectful.Tracing.Instrumentation.Database
( DatabaseQuery (queryCollection, queryOperation, queryText)
, databaseQuery
, withQuerySpan
)
import Effectful.Tracing.Instrumentation.Messaging
( MessagingOperation (messagingDestination)
, MessagingOperationType (Process, Send)
, injectMessageHeaders
, messagingOperation
, withConsumerSpan
, withMessagingSpan
)
import Effectful.Tracing.Propagation.B3 (extractContextB3, injectContextB3)
import Effectful.Tracing.Propagation.Baggage (extractBaggage, injectBaggage)
import Effectful.Tracing.Propagation.Composite
( BaggagePropagator
, TraceContextPropagator
, b3Single
, extractBaggageAll
, extractContextFirst
, injectBaggageAll
, injectContextAll
, jaegerBaggage
, w3cBaggage
, w3cTraceContext
)
import Effectful.Tracing.Log (activeCorrelationFields)
import Effectful.Tracing.Propagation.Jaeger
( extractBaggageJaeger
, extractContextJaeger
, injectContextJaeger
)
import Effectful.Tracing.Testing
( findSpan
, hasStatus
, isChildOf
, isRoot
, lookupAttribute
, runTracerInMemory
)
import Effectful.Tracing.Concurrent (concurrentlyInstrumented, forkLinked)
import Effectful.Tracing.Interpreter.InMemory
( newCapturedSpans
, readCapturedSpans
, runTracerInMemoryWith
, runTracerInMemoryWithLimits
)
import Effectful.Tracing.Interpreter.NoOp (runTracerNoOp)
import Effectful.Tracing.Interpreter.PrettyPrint
( PrettyPrintConfig (showEvents, timeFormat, useColor)
, TimeFormat (RelativeToTraceStart)
, defaultPrettyPrintConfig
, runTracerPretty
)
import Effectful.Tracing.Sampler
( Sampler (Sampler, samplerName, shouldSample)
, SamplerInput (initialAttributes)
, SamplingDecision (RecordAndSample)
, simpleResult
)
import Effectful.Tracing.SpanLimits
( SpanLimits (attributeValueLengthLimit, eventCountLimit)
, defaultSpanLimits
)
#ifdef WAI
import Data.Text.Encoding (decodeUtf8Lenient)
import Network.Wai (Application, Request, rawPathInfo, requestMethod)
import Effectful.Tracing.Instrumentation.Wai (traceMiddleware, traceMiddlewareWith)
#endif
#ifdef HTTP_CLIENT
import Network.HTTP.Client (Manager, Response, parseRequest)
import Data.ByteString.Lazy (ByteString)
import Effectful.Tracing.Instrumentation.HttpClient (httpLbsTraced)
#endif
#ifdef POSTGRESQL_SIMPLE
import Database.PostgreSQL.Simple (Connection, Only (..))
import Effectful.Tracing.Instrumentation.PostgresqlSimple qualified as Pg
#endif
#ifdef SQLITE_SIMPLE
import Database.SQLite.Simple qualified as SqliteDb
import Effectful.Tracing.Instrumentation.SqliteSimple qualified as Sqlite
#endif
#ifdef VALIANT
import Data.Int (Int64)
import Valiant (Statement, mkStatement)
import Valiant.Effectful (Valiant)
import Effectful.Tracing.Instrumentation.Valiant qualified as ValiantT
#endif
#ifdef AMQP_BINDING
import Network.AMQP (Ack (Ack), Channel, Message (msgBody), newMsg)
import Effectful.Tracing.Instrumentation.Amqp qualified as Amqp
#endif
#ifdef SERVANT
import Data.Proxy (Proxy (Proxy))
#ifndef WAI
import Network.Wai (Application)
#endif
import Servant (Capture, Get, PlainText, Server, serve, type (:<|>) ((:<|>)))
import Servant qualified as Sv
import Effectful.Tracing.Instrumentation.Servant (WithSpanName, traceServantMiddleware)
#endif
#ifdef OTEL
import Effectful.Tracing.Interpreter.OpenTelemetry (OtelConfig (..), runTracerOTel)
#endif
tests :: TestTree
tests =
testGroup
"Compile-only checks"
[ testCase "smart-constructor API typechecks" (compiles @?= ())
, testCase "documentation examples typecheck" (docExamples @?= ())
]
-- | Forcing the example programs to a concrete effect stack references them
-- (so @-Wunused-top-binds@ stays quiet) and pins their otherwise-polymorphic
-- types, without running them.
compiles :: ()
compiles =
(nestedSpans :: Eff '[Tracer, IOE] Int)
`seq` (spanWithArguments :: Eff '[Tracer] ())
`seq` ()
-- | Nested spans with attribute, event, and status annotations.
nestedSpans :: Tracer :> es => Eff es Int
nestedSpans = withSpan "outer" $ do
addAttribute "user.id" ("u123" :: Text)
result <- withSpan "inner" $ do
addEvent "fetching" []
pure (42 :: Int)
setStatus Ok
pure result
-- | Exercises 'withSpan'' with explicit arguments, the remaining emit
-- operations, and 'getActiveSpan'.
spanWithArguments :: Tracer :> es => Eff es ()
spanWithArguments =
withSpan' "http.get" defaultSpanArguments {kind = Client} $ do
addAttributes ["http.method" .= ("GET" :: Text), "http.status_code" .= (200 :: Int)]
recordException (toException (userError "transient"))
setStatus (Error "upstream timeout")
active <- getActiveSpan
case active :: Maybe SpanContext of
Nothing -> pure ()
Just _ -> pure ()
-- ---------------------------------------------------------------------------
-- Documentation examples
--
-- One binding per code block in the docs, named for its source. References to
-- application-specific types and effects (a database, a job queue, an order
-- record) are stubbed once here so the library calls keep their documented
-- shapes.
-- ---------------------------------------------------------------------------
-- | References every doc mirror at a concrete type so the block must typecheck.
-- Flag-gated interpreters and instrumentation are folded in conditionally.
docExamples :: ()
docExamples =
(cbLoadUser :: UserId -> Eff '[Database, Tracer] User)
`seq` (cbHandleOrder :: Order -> Eff '[Tracer] ())
`seq` (cbPublishOrder :: Eff '[Tracer] [(Text, Text)])
`seq` (cbConsumeOrder :: [(Text, Text)] -> Eff '[Tracer] () -> Eff '[Tracer] ())
`seq` samplerName cbPriorityOr1Percent
`seq` (cbRiskyCharge :: Eff '[Tracer] ())
`seq` (cbPrioritySampledRun :: Eff '[Tracer, IOE] () -> IO [Span])
`seq` (cbConsume :: [Header] -> Eff '[Tracer] () -> Eff '[Tracer] ())
`seq` (cbB3Consume :: [Header] -> Eff '[Tracer] () -> Eff '[Tracer] ())
`seq` (cbB3Forward :: Eff '[Tracer] [Header])
`seq` (cbJaegerConsume :: [Header] -> Eff '[BaggageContext, Tracer] () -> Eff '[Tracer] ())
`seq` (cbJaegerForward :: Eff '[Tracer] [Header])
`seq` (cbCompositeConsume :: [Header] -> Eff '[Tracer] () -> Eff '[Tracer] ())
`seq` (cbCompositeForward :: Eff '[Tracer] [Header])
`seq` (cbCompositeServe :: [Header] -> Eff '[BaggageContext] () -> Eff '[] ())
`seq` (cbCompositeBaggageForward :: Eff '[BaggageContext] [Header])
`seq` (cbEnvConfigMain :: IO ())
`seq` (cbServeWithBaggage :: [Header] -> Eff '[BaggageContext, Tracer] () -> Eff '[Tracer] ())
`seq` (cbPriorityOf :: Eff '[BaggageContext] (Maybe Text))
`seq` (cbHandleBaggage :: Eff '[BaggageContext, Tracer] [Header])
`seq` (cbCheckHandlerTrace :: IO ())
`seq` (cbLogEvent :: (Text -> [(Text, Text)] -> Eff '[Tracer] ()) -> Text -> Eff '[Tracer] ())
`seq` (cbHandleRequest :: (Text -> [(Text, Text)] -> Eff '[Tracer] ()) -> Eff '[Tracer] ())
`seq` (cbFetchActiveUsers :: Eff '[Tracer, IOE] [(Int, Text)])
`seq` (cbWorker :: Eff '[Queue, Tracer] ())
`seq` (cbEnqueueBackground :: Eff '[Tracer, Concurrent] ())
`seq` (cbPrettyRun :: Eff '[Tracer, IOE] () -> IO ())
`seq` (cbBoundedRun :: Eff '[Tracer, IOE] () -> IO [Span])
`seq` (tutCheckout :: Eff '[Tracer] Int)
`seq` (tutAnnotated :: Eff '[Tracer] ())
`seq` (tutSampledRun :: Eff '[Tracer, IOE] () -> IO [Span])
`seq` (tutFanOut :: Eff '[Tracer, Concurrent] (Int, Int))
`seq` (readmeCompute :: Eff '[Tracer] Int)
`seq` (readmeNoOpMain :: IO ())
`seq` (readmePrettyMain :: IO ())
`seq` stubConstructors
`seq` waiExamples
`seq` httpClientExamples
`seq` postgresqlSimpleExamples
`seq` sqliteSimpleExamples
`seq` valiantExamples
`seq` amqpExamples
`seq` servantExamples
`seq` otelExamples
`seq` ()
-- Stub application types/effects the docs reference.
-- | The docs use only the field selectors of the stub types, never their
-- constructors, so reference the constructors here to keep @-Wunused-top-binds@
-- quiet. Never evaluated beyond WHNF.
stubConstructors :: (UserId, User, Job, Order)
stubConstructors = (UserId "", User, Job "", Order "" 0 "" False 0)
newtype UserId = UserId Text
data User = User
newtype Job = Job {jobId :: Text}
data Order = Order
{ orderId :: Text
, totalCents :: Int
, tierName :: Text
, isExpress :: Bool
, lineCount :: Int
}
data Database :: Effect where
QueryUser :: UserId -> Database m User
type instance DispatchOf Database = Dynamic
queryUser :: Database :> es => UserId -> Eff es User
queryUser = send . QueryUser
data Queue :: Effect where
TakeJob :: Queue m Job
type instance DispatchOf Queue = Dynamic
takeJob :: Queue :> es => Eff es Job
takeJob = send TakeJob
-- cookbook: "Trace an existing function"
cbLoadUser :: (Database :> es, Tracer :> es) => UserId -> Eff es User
cbLoadUser uid = withSpan "loadUser" $ queryUser uid
-- cookbook: "Attach structured fields"
cbHandleOrder :: Tracer :> es => Order -> Eff es ()
cbHandleOrder order = withSpan "handleOrder" $ do
addAttribute "order.id" (orderId order)
addAttribute "order.total_cents" (totalCents order)
addAttributes
[ "customer.tier" .= tierName order
, "order.express" .= isExpress order
, "order.line_count" .= lineCount order
]
addEvent "payment.authorized" ["gateway" .= ("stripe" :: Text)]
-- cookbook: "Trace a message producer and consumer" (producer)
cbPublishOrder :: (Tracer :> es) => Eff es [(Text, Text)]
cbPublishOrder =
withMessagingSpan
(messagingOperation "kafka" Send) {messagingDestination = Just "orders"}
injectMessageHeaders
-- cookbook: "Trace a message producer and consumer" (consumer)
cbConsumeOrder :: (Tracer :> es) => [(Text, Text)] -> Eff es a -> Eff es a
cbConsumeOrder headers =
withConsumerSpan headers (messagingOperation "kafka" Process) {messagingDestination = Just "orders"}
-- cookbook: "Sample but keep what matters" (custom sampler)
cbPriorityOr1Percent :: Sampler
cbPriorityOr1Percent =
Sampler
{ samplerName = "PriorityOr1Percent"
, shouldSample = \input ->
if flagged (initialAttributes input)
then pure (simpleResult RecordAndSample)
else shouldSample (traceIdRatioBased 0.01) input
}
where
flagged = any (\(Attribute k v) -> k == "sampling.priority" && v == AttrBool True)
-- cookbook: "Sample but keep what matters" (flag a span)
cbRiskyCharge :: Tracer :> es => Eff es ()
cbRiskyCharge =
withSpan' "charge" defaultSpanArguments {attributes = ["sampling.priority" .= True]} $
pure ()
-- cookbook: "Sample but keep what matters" (run with the custom sampler)
cbPrioritySampledRun :: Eff '[Tracer, IOE] a -> IO [Span]
cbPrioritySampledRun action = runEff $ do
captured <- newCapturedSpans
_ <- runTracerInMemoryWith cbPriorityOr1Percent captured action
readCapturedSpans captured
-- cookbook: "Connect inbound and outbound HTTP traces" (continue a remote parent)
cbConsume :: Tracer :> es => [Header] -> Eff es a -> Eff es a
cbConsume headers =
maybe id withRemoteParent (extractContext headers)
-- cookbook: "Interoperate with B3 (Zipkin) headers" (continue a B3 remote parent)
cbB3Consume :: Tracer :> es => [Header] -> Eff es a -> Eff es a
cbB3Consume headers =
maybe id withRemoteParent (extractContextB3 headers)
-- cookbook: "Interoperate with B3 (Zipkin) headers" (forward as a single b3 header)
cbB3Forward :: Tracer :> es => Eff es [Header]
cbB3Forward = injectContextB3
-- cookbook: "Interoperate with Jaeger (uber-trace-id) headers" (continue + baggage)
cbJaegerConsume :: Tracer :> es => [Header] -> Eff (BaggageContext : es) a -> Eff es a
cbJaegerConsume headers =
runBaggageWith (extractBaggageJaeger headers)
. maybe id withRemoteParent (extractContextJaeger headers)
-- cookbook: "Interoperate with Jaeger (uber-trace-id) headers" (forward uber-trace-id)
cbJaegerForward :: Tracer :> es => Eff es [Header]
cbJaegerForward = injectContextJaeger
-- cookbook: "Combine several propagators" (the configured trace-context list)
cbPropagators :: [TraceContextPropagator]
cbPropagators = [w3cTraceContext, b3Single]
-- cookbook: "Combine several propagators" (continue the first matching format)
cbCompositeConsume :: Tracer :> es => [Header] -> Eff es a -> Eff es a
cbCompositeConsume headers =
maybe id withRemoteParent (extractContextFirst cbPropagators headers)
-- cookbook: "Combine several propagators" (emit every configured format)
cbCompositeForward :: Tracer :> es => Eff es [Header]
cbCompositeForward = injectContextAll cbPropagators
-- cookbook: "Combine several propagators" (merge baggage from every format)
cbCompositeServe :: [Header] -> Eff (BaggageContext : es) a -> Eff es a
cbCompositeServe headers =
runBaggageWith (extractBaggageAll cbBaggagePropagators headers)
-- cookbook: "Combine several propagators" (forward baggage in every format)
cbCompositeBaggageForward :: BaggageContext :> es => Eff es [Header]
cbCompositeBaggageForward = injectBaggageAll cbBaggagePropagators
-- cookbook: "Combine several propagators" (the configured baggage list)
cbBaggagePropagators :: [BaggagePropagator]
cbBaggagePropagators = [w3cBaggage, jaegerBaggage]
-- cookbook: "Configure tracing from OTEL_ environment variables"
cbEnvConfigMain :: IO ()
cbEnvConfigMain = do
env <- readEnvConfig
let _name = serviceName env
_attrs = resourceAttributes env
_propagators = traceContextPropagators env
_sampler = tracesSampler env
pure ()
-- cookbook: "Stamp logs with the active trace and span"
cbLogEvent :: Tracer :> es => (Text -> [(Text, Text)] -> Eff es ()) -> Text -> Eff es ()
cbLogEvent emit message = do
fields <- activeCorrelationFields
emit message fields
cbHandleRequest :: Tracer :> es => (Text -> [(Text, Text)] -> Eff es ()) -> Eff es ()
cbHandleRequest emit = withSpan "handle" $ cbLogEvent emit "handling request"
-- cookbook: "Trace a database query" (framework-agnostic withQuerySpan)
cbFetchActiveUsers :: (IOE :> es, Tracer :> es) => Eff es [(Int, Text)]
cbFetchActiveUsers =
withQuerySpan
(databaseQuery "postgresql")
{ queryText = Just "SELECT id, name FROM users WHERE active = $1"
, queryOperation = Just "SELECT"
, queryCollection = Just "users"
}
(liftIO rawSelectActiveUsers)
-- | Stands in for a driver's own query call in the database cookbook mirror;
-- only forced to WHNF, never run.
rawSelectActiveUsers :: IO [(Int, Text)]
rawSelectActiveUsers = error "compile-only: doc mirror is never run"
-- cookbook: "Carry application context as baggage" (seed inbound baggage)
cbServeWithBaggage :: [Header] -> Eff (BaggageContext : es) a -> Eff es a
cbServeWithBaggage headers = runBaggageWith (extractBaggage headers)
-- cookbook: "Carry application context as baggage" (read a value in scope)
cbPriorityOf :: BaggageContext :> es => Eff es (Maybe Text)
cbPriorityOf = lookupBaggageValue "request.priority" <$> getBaggage
-- cookbook: "Carry application context as baggage" (scope an entry, forward it)
cbHandleBaggage :: (BaggageContext :> es, Tracer :> es) => Eff es [Header]
cbHandleBaggage = withBaggageEntry "request.priority" "high" $ withSpan "handle" injectBaggage
-- cookbook: "Assert on traces in your tests"
cbCheckHandlerTrace :: IO ()
cbCheckHandlerTrace = do
spans <- runEff $ do
captured <- newCapturedSpans
runTracerInMemory captured $
withSpan "handler" $ do
setStatus Ok
withSpan "db.query" (addAttribute "db.rows" (1 :: Int))
readCapturedSpans captured
case (findSpan "handler" spans, findSpan "db.query" spans) of
(Just handler, Just db) -> do
isRoot handler @?= True
(db `isChildOf` handler) @?= True
hasStatus Ok handler @?= True
lookupAttribute "db.rows" db @?= Just (AttrInt 1)
_ -> pure ()
-- cookbook: "Instrument a long-running worker"
cbWorker :: (Tracer :> es, Queue :> es) => Eff es ()
cbWorker = do
job <- takeJob
withSpan "worker.handleJob" $ do
addAttribute "job.id" (jobId job)
pure ()
-- cookbook: "Instrument a long-running worker" (linked background work)
cbEnqueueBackground :: (Tracer :> es, Concurrent :> es) => Eff es ()
cbEnqueueBackground = withSpan "request" $ do
_ <- forkLinked (withSpan "background.reindex" (pure ()))
pure ()
-- cookbook: pretty-print configuration
cbPrettyRun :: Eff '[Tracer, IOE] a -> IO a
cbPrettyRun action = do
color <- hIsTerminalDevice stderr
let config =
(defaultPrettyPrintConfig stderr)
{ useColor = color
, showEvents = False
, timeFormat = RelativeToTraceStart
}
runEff (runTracerPretty config action)
-- cookbook: "Bound what a span records"
cbBoundedRun :: Eff '[Tracer, IOE] a -> IO [Span]
cbBoundedRun action = runEff $ do
captured <- newCapturedSpans
let limits = defaultSpanLimits {attributeValueLengthLimit = Just 1024, eventCountLimit = Just 64}
_ <- runTracerInMemoryWithLimits limits alwaysOn captured action
readCapturedSpans captured
-- tutorial: first traced computation
tutCheckout :: Tracer :> es => Eff es Int
tutCheckout = withSpan "checkout" $ do
addAttribute "cart.items" (3 :: Int)
total <- withSpan "price.total" (pure 4200)
setStatus Ok
pure total
-- tutorial: annotating a span
tutAnnotated :: Tracer :> es => Eff es ()
tutAnnotated = withSpan "handle.order" $ do
addAttribute "order.id" ("o-9921" :: Text)
addAttributes ["http.method" .= ("POST" :: Text), "http.status_code" .= (200 :: Int)]
addEvent "inventory.reserved" ["sku" .= ("widget-1" :: Text)]
recordException (toException (ErrorCall "downstream slow"))
setStatus (Error "downstream slow")
-- tutorial: deterministic ratio sampling
tutSampledRun :: Eff '[Tracer, IOE] a -> IO [Span]
tutSampledRun action = runEff $ do
captured <- newCapturedSpans
_ <- runTracerInMemoryWith (traceIdRatioBased 0.1) captured action
readCapturedSpans captured
-- tutorial: concurrent fan-out under one parent
tutFanOut :: (Tracer :> es, Concurrent :> es) => Eff es (Int, Int)
tutFanOut = withSpan "fan.out" $
concurrentlyInstrumented
(withSpan "left" (pure 1))
(withSpan "right" (pure 2))
-- README: tracing without committing to a backend
readmeCompute :: Tracer :> es => Eff es Int
readmeCompute = withSpan "outer" $ do
addAttribute "user.id" ("u123" :: Text)
total <- withSpan "inner" $ do
addEvent "fetching" []
pure 42
setStatus Ok
pure total
-- README: no-op interpreter
readmeNoOpMain :: IO ()
readmeNoOpMain = do
result <- runEff (runTracerNoOp readmeCompute)
print result
-- README: pretty-print interpreter
readmePrettyMain :: IO ()
readmePrettyMain = do
result <- runEff (runTracerPretty (defaultPrettyPrintConfig stderr) readmeCompute)
print result
-- WAI middleware examples (only when built with +wai).
waiExamples :: ()
#ifdef WAI
waiExamples =
(cbRouteName :: Request -> Text)
`seq` (waiWrap stubRunInIO stubApp :: Application)
`seq` (waiWrapNamed stubRunInIO stubApp :: Application)
`seq` ()
where
-- Stubs let the rank-2 mirrors be referenced as a plain 'Application'; both
-- are only forced to WHNF (a partial application), so neither is evaluated.
stubRunInIO :: Eff '[IOE, Tracer] a -> IO a
stubRunInIO = error "compile-only: doc mirror is never run"
stubApp :: Application
stubApp = error "compile-only: doc mirror is never run"
-- cookbook: name a server span after the matched route
cbRouteName :: Request -> Text
cbRouteName req =
decodeUtf8Lenient (requestMethod req) <> " " <> decodeUtf8Lenient (rawPathInfo req)
-- README / tutorial: wrap a WAI app with the tracing middleware
waiWrap :: (IOE :> es, Tracer :> es) => (forall a. Eff es a -> IO a) -> Application -> Application
waiWrap runInIO app = traceMiddleware runInIO app
-- cookbook: middleware with a custom span name
waiWrapNamed :: (IOE :> es, Tracer :> es) => (forall a. Eff es a -> IO a) -> Application -> Application
waiWrapNamed runInIO app = traceMiddlewareWith cbRouteName runInIO app
#else
waiExamples = ()
#endif
-- http-client examples (only when built with +http-client).
httpClientExamples :: ()
#ifdef HTTP_CLIENT
httpClientExamples =
(cbFetchProfile :: Manager -> Eff '[Tracer, IOE] (Response ByteString))
`seq` ()
-- README / cookbook / tutorial: traced outbound request
cbFetchProfile :: (IOE :> es, Tracer :> es) => Manager -> Eff es (Response ByteString)
cbFetchProfile manager = withSpan "load.profile" $ do
req <- liftIO (parseRequest "http://users.internal/profile")
httpLbsTraced req manager
#else
httpClientExamples = ()
#endif
-- postgresql-simple examples (only when built with +postgresql-simple).
postgresqlSimpleExamples :: ()
#ifdef POSTGRESQL_SIMPLE
postgresqlSimpleExamples =
(cbActiveUserNames :: Connection -> Eff '[Tracer, IOE] [Only Text])
`seq` ()
-- cookbook: traced postgresql-simple query
cbActiveUserNames :: (IOE :> es, Tracer :> es) => Connection -> Eff es [Only Text]
cbActiveUserNames conn =
Pg.query conn "SELECT name FROM users WHERE active = ?" (Only True)
#else
postgresqlSimpleExamples = ()
#endif
-- sqlite-simple examples (only when built with +sqlite-simple).
sqliteSimpleExamples :: ()
#ifdef SQLITE_SIMPLE
sqliteSimpleExamples =
(cbSqliteActiveUserNames :: SqliteDb.Connection -> Eff '[Tracer, IOE] [SqliteDb.Only Text])
`seq` (cbSqliteSeedUsers :: SqliteDb.Connection -> Eff '[Tracer, IOE] ())
`seq` ()
-- cookbook: traced sqlite-simple query
cbSqliteActiveUserNames :: (IOE :> es, Tracer :> es) => SqliteDb.Connection -> Eff es [SqliteDb.Only Text]
cbSqliteActiveUserNames conn =
Sqlite.query conn "SELECT name FROM users WHERE active = ?" (SqliteDb.Only True)
-- cookbook: traced sqlite-simple batch insert
cbSqliteSeedUsers :: (IOE :> es, Tracer :> es) => SqliteDb.Connection -> Eff es ()
cbSqliteSeedUsers conn =
Sqlite.executeMany conn "INSERT INTO users (name) VALUES (?)" [SqliteDb.Only ("a" :: Text), SqliteDb.Only "b"]
#else
sqliteSimpleExamples = ()
#endif
-- valiant examples (only when built with +valiant).
valiantExamples :: ()
#ifdef VALIANT
valiantExamples =
(cbValiantActiveUsers :: Eff '[Valiant, Tracer] [Text])
`seq` (cbValiantSeedUsers :: Eff '[Valiant, Tracer] Int64)
`seq` ()
-- cookbook: traced valiant query
cbValiantActiveUsers :: (Valiant :> es, Tracer :> es) => Eff es [Text]
cbValiantActiveUsers = ValiantT.fetchAllEff listActiveUsers ()
-- cookbook: traced valiant batch insert
cbValiantSeedUsers :: (Valiant :> es, Tracer :> es) => Eff es Int64
cbValiantSeedUsers = ValiantT.executeBatchEff insertUser ["a", "b"]
-- | Stand-in statements for the valiant mirror. The plugin would normally
-- build these from validated SQL; 'mkStatement' constructs them directly so
-- the mirror needs no plugin or database. Never run.
listActiveUsers :: Statement () Text
listActiveUsers = mkStatement "SELECT name FROM users WHERE active = true" [] ["name"] "inline"
insertUser :: Statement Text ()
insertUser = mkStatement "INSERT INTO users (name) VALUES ($1)" [25] [] "inline"
#else
valiantExamples = ()
#endif
-- amqp examples (only when built with +amqp).
amqpExamples :: ()
#ifdef AMQP_BINDING
amqpExamples =
(cbAmqpPublishOrder :: Channel -> Eff '[Tracer, IOE] (Maybe Int))
`seq` (cbAmqpHandleOrder :: Channel -> Eff '[Tracer, IOE] ())
`seq` ()
-- cookbook: traced amqp publish
cbAmqpPublishOrder :: (IOE :> es, Tracer :> es) => Channel -> Eff es (Maybe Int)
cbAmqpPublishOrder chan =
Amqp.publishMsgTraced chan "orders" "orders.created" newMsg {msgBody = "{}"}
-- cookbook: traced amqp receive then process
cbAmqpHandleOrder :: (IOE :> es, Tracer :> es) => Channel -> Eff es ()
cbAmqpHandleOrder chan = do
received <- Amqp.getMsgTraced chan Ack "orders"
case received of
Nothing -> pure ()
Just (msg, env) -> Amqp.withProcessSpan msg env (pure ())
#else
amqpExamples = ()
#endif
-- servant examples (only when built with +servant).
servantExamples :: ()
#ifdef SERVANT
servantExamples =
(servantServer :: Server ServantApi)
`seq` (servantWrap stubRunInIOS (serve (Proxy :: Proxy ServantApi) servantServer) :: Application)
`seq` ()
where
stubRunInIOS :: Eff '[IOE, Tracer] a -> IO a
stubRunInIOS = error "compile-only: doc mirror is never run"
-- cookbook: name server spans from the matched route with Servant
type ServantApi =
WithSpanName "/users/{id}" Sv.:> "users" Sv.:> Capture "id" Int Sv.:> Get '[PlainText] Text
:<|> WithSpanName "/health" Sv.:> "health" Sv.:> Get '[PlainText] Text
servantServer :: Server ServantApi
servantServer = (\_ -> pure "user") :<|> pure "ok"
servantWrap
:: (IOE :> es, Tracer :> es)
=> (forall a. Eff es a -> IO a)
-> Application
-> Application
servantWrap runInIO app = traceServantMiddleware runInIO app
#else
servantExamples = ()
#endif
-- OpenTelemetry export example (only when built with +otel). The exporter and
-- batch processor in the doc come from the SDK packages, which are not test
-- dependencies; the library surface (the 'OtelConfig' record and
-- 'runTracerOTel') is what this mirror pins, with an empty processor list.
otelExamples :: ()
#ifdef OTEL
otelExamples =
(tutOtelRun :: Eff '[Tracer, IOE] Int -> IO Int)
`seq` ()
tutOtelRun :: Eff '[Tracer, IOE] a -> IO a
tutOtelRun action = do
let config =
OtelConfig
{ spanProcessors = []
, instrumentationScope = "checkout-service"
, sampler = parentBased (defaultParentBasedConfig alwaysOn)
, spanLimits = defaultSpanLimits
}
runEff (runTracerOTel config action)
#else
otelExamples = parentBased (defaultParentBasedConfig alwaysOn) `seq` ()
#endif