effectful-tracing-0.1.0.0: test/Effectful/Tracing/Instrumentation/HttpClientSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Effectful.Tracing.Instrumentation.HttpClientSpec
-- Description : Tests for the http-client tracing wrapper.
--
-- These run against a real loopback server (a tiny WAI app served by Warp on an
-- ephemeral port via 'Warp.testWithApplication') so the wrapper exercises an
-- actual request/response. The server records the headers it received, which
-- lets us assert that the wrapper injected a @traceparent@ carrying the client
-- span's trace id (propagation end to end), and the captured spans let us assert
-- the @client@ kind, the stable HTTP \/ URL attributes, and the status mapping.
module Effectful.Tracing.Instrumentation.HttpClientSpec
( tests
) where
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar)
import Control.Monad (void)
import Data.List (find, isInfixOf)
import Data.Text qualified as T
import Network.HTTP.Client
( Manager
, Request
, defaultManagerSettings
, newManager
, parseRequest
)
import Network.HTTP.Types (Status, status200, status500)
import Network.HTTP.Types.Header (RequestHeaders)
import Network.Wai (Application, requestHeaders, responseLBS)
import Network.Wai.Handler.Warp (Port)
import Network.Wai.Handler.Warp qualified as Warp
import Effectful (Eff, IOE, runEff, (:>))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
import Effectful.Tracing
( Span (spanAttributes, spanContext, spanKind, spanStatus)
, SpanContext (spanContextTraceId)
, SpanKind (Client)
, SpanStatus (Error, Unset)
, Tracer
, extractContext
, traceIdToHex
, withSpan
)
import Effectful.Tracing.Attribute (Attribute (Attribute), AttributeValue (AttrInt, AttrText))
import Effectful.Tracing.Instrumentation.HttpClient (httpLbsTraced)
import Effectful.Tracing.Interpreter.InMemory
( newCapturedSpans
, readCapturedSpans
, runTracerInMemory
)
tests :: TestTree
tests =
testGroup
"Instrumentation.HttpClient"
[ testCase "opens a client span and records http attributes" $
withEchoServer status200 $ \captured port -> do
(spans, _) <- runClient port "/widgets"
s <- clientSpan spans
lookupText "http.request.method" s @?= Just "GET"
assertBool
"url.full contains the request path"
(maybe False (("/widgets" `isInfixOf`) . T.unpack) (lookupText "url.full" s))
lookupInt "http.response.status_code" s @?= Just 200
spanStatus s @?= Unset
-- The server received our traceparent, carrying the client span's trace.
headers <- readMVar captured
remote <- maybe (assertFailure "server saw no traceparent") pure (extractContext headers)
traceIdToHex (spanContextTraceId remote)
@?= traceIdToHex (spanContextTraceId (spanContext s))
, testCase "a >= 400 response sets the span status to error" $
withEchoServer status500 $ \_ port -> do
(spans, _) <- runClient port "/boom"
s <- clientSpan spans
lookupInt "http.response.status_code" s @?= Just 500
case spanStatus s of
Error _ -> pure ()
other -> assertFailure ("expected Error status, got " <> show other)
]
-- | Serve a tiny app on an ephemeral port that records the inbound headers and
-- replies with the given status, then run the continuation against it.
withEchoServer :: Status -> (MVar RequestHeaders -> Port -> IO a) -> IO a
withEchoServer responseStatus k = do
captured <- newMVar []
let app :: Application
app req respond = do
modifyMVar_ captured (const (pure (requestHeaders req)))
respond (responseLBS responseStatus [] "ok")
Warp.testWithApplication (pure app) (k captured)
-- | Send a traced GET to the loopback server, returning the captured spans.
runClient :: Port -> String -> IO ([Span], ())
runClient port path = do
manager <- newManager defaultManagerSettings
req <- parseRequest ("http://localhost:" <> show port <> path)
spans <- runEff $ do
cap <- newCapturedSpans
_ <- runTracerInMemory cap (call manager req)
readCapturedSpans cap
pure (spans, ())
where
call :: (Tracer :> es, IOE :> es) => Manager -> Request -> Eff es ()
call manager req = withSpan "outer" (void (httpLbsTraced req manager))
-- | The single @client@-kind span among the captured spans.
clientSpan :: [Span] -> IO Span
clientSpan spans =
maybe (assertFailure "expected a client span") pure (find ((== Client) . spanKind) spans)
lookupText :: T.Text -> Span -> Maybe T.Text
lookupText key s =
case findAttr key s of
Just (AttrText t) -> Just t
_ -> Nothing
lookupInt :: T.Text -> Span -> Maybe Int
lookupInt key s =
case findAttr key s of
Just (AttrInt n) -> Just (fromIntegral n)
_ -> Nothing
findAttr :: T.Text -> Span -> Maybe AttributeValue
findAttr key s =
fmap (\(Attribute _ v) -> v) (find (\(Attribute k _) -> k == key) (spanAttributes s))