aws-xray-client-wai-0.1.0.0: library/Network/AWS/XRayClient/WAI.hs
{-# LANGUAGE NamedFieldPuns #-}
-- | Module for using a WAI Middleware as an X-Ray client.
module Network.AWS.XRayClient.WAI
( XRayClientConfig(..)
, xrayClientConfig
, xrayTraceMiddleware
, NoAddressInfoException(..)
, xrayWaiVaultKey
, vaultDataFromRequest
, XRayVaultData(..)
, traceXRaySubsegment
, traceXRaySubsegment'
, atomicallyAddVaultDataSubsegment
, makeSubsegmentIndependent
, module X
) where
import Prelude
import Control.Concurrent (forkIO)
import Control.Lens hiding ((|>))
import Control.Monad (void, when)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.IO.Unlift (MonadUnliftIO)
import qualified Data.ByteString.Char8 as BS8
import Data.Foldable (toList)
import Data.IORef
import Data.Monoid ((<>))
import Data.Sequence (Seq, (|>))
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock.POSIX
import qualified Data.Vault.Lazy as V
import Network.AWS.XRayClient.Segment as X
import Network.AWS.XRayClient.SendSegments as X
import Network.AWS.XRayClient.TraceId as X
import Network.HTTP.Types.Status (statusCode)
import Network.Wai
import System.IO.Unsafe (unsafePerformIO)
import System.Random
import System.Random.XRayCustom
import UnliftIO.Exception (finally)
-- | Configuration type for the XRay client middleware.
data XRayClientConfig
= XRayClientConfig
{ xrayClientConfigDaemonHost :: !Text
-- ^ The host that the daemon is listening on.
, xrayClientConfigDaemonPort :: !Int
-- ^ The port that the daemon is listening on.
, xrayClientConfigApplicationName :: !Text
-- ^ The value of the "name" field that will be sent to X-Ray.
, xrayClientConfigSampler :: !(Maybe (Request -> Response -> POSIXTime -> POSIXTime -> IO Bool))
-- ^ A sampling function to filter traces.
}
-- | Constructor for 'XRayClientConfig' with required arguments.
xrayClientConfig :: Text -> XRayClientConfig
xrayClientConfig appName = XRayClientConfig
{ xrayClientConfigDaemonHost = "127.0.0.1"
, xrayClientConfigDaemonPort = 2000
, xrayClientConfigApplicationName = appName
, xrayClientConfigSampler = Nothing
}
-- | Traces the execution time of a request and sends the the local X-Ray
-- daemon.
xrayTraceMiddleware :: XRayClientConfig -> Middleware
xrayTraceMiddleware clientConfig@XRayClientConfig {..} app request respond = do
-- Start timer. We purposely include all of this setup in the time.
startTime <- getPOSIXTime
-- Create an IORef StdGen. We share this across the life of the request so we
-- don't have multiple simultaneous requests causing contention on the global
-- StdGen.
stdGenIORef <- newStdGen >>= newIORef
-- Check for the X-Amzn-Trace-Id and try to parse it. If that isn't possible,
-- then make a new header.
let
oldHeaders = requestHeaders request
mTraceIdHeaderData =
lookup amazonTraceIdHeaderName oldHeaders >>= parseXRayTraceIdHeaderData
(newHeaders, headerData@XRayTraceIdHeaderData {..}) <-
case mTraceIdHeaderData of
Just headerData -> pure (oldHeaders, headerData)
Nothing -> do
traceId <- generateXRayTraceId stdGenIORef
let
headerData = xrayTraceIdHeaderData traceId
header =
(amazonTraceIdHeaderName, makeXRayTraceIdHeaderValue headerData)
pure (header : oldHeaders, headerData)
-- Construct the new request including the vault data
segmentId <- withRandomGenIORef stdGenIORef generateXRaySegmentId
subsegmentsIORef <- newIORef Seq.empty
let
vaultData = XRayVaultData
headerData
clientConfig
segmentId
stdGenIORef
subsegmentsIORef
request' = request
{ requestHeaders = newHeaders
, vault = V.insert xrayWaiVaultKey vaultData (vault request)
}
-- Run application with the new Request
app request' $ \response -> do
responseAccept <- respond response
-- Create segment
endTime <- getPOSIXTime
subsegments <- readIORef subsegmentsIORef
let
segment =
xraySegment
xrayClientConfigApplicationName
segmentId
xrayTraceIdHeaderDataRootTraceId
startTime
(Just endTime)
& xraySegmentParentId
.~ xrayTraceIdHeaderDataParentId
& addRequestToSegment request'
& addResponseToSegment response
independentSubsegments =
makeSubsegmentIndependent vaultData <$> toList subsegments
-- Send the segment if it passes the filters
shouldSend <- maybe
(pure True)
(\f -> f request response startTime endTime)
xrayClientConfigSampler
when shouldSend $ void $ forkIO $ sendSegmentsToDaemon
xrayClientConfigDaemonHost
xrayClientConfigDaemonPort
(segment : independentSubsegments)
-- Be a good middleware and return the original ResponseAccept
pure responseAccept
-- | Adds the info from a WAI 'Request' to an 'XRaySegment'.
addRequestToSegment :: Request -> XRaySegment -> XRaySegment
addRequestToSegment request =
xraySegmentHttp
. non xraySegmentHttpDef
. xraySegmentHttpRequest
. non xraySegmentHttpRequestDef
%~ ((xraySegmentHttpRequestMethod
?~ T.pack (BS8.unpack (requestMethod request))
)
. (xraySegmentHttpRequestUrl ?~ "/" <> T.intercalate
"/"
(pathInfo request)
)
. (xraySegmentHttpRequestUserAgent
.~ (T.pack . BS8.unpack <$> requestHeaderUserAgent request)
)
)
-- | Adds the info from a WAI 'Response' to an 'XRaySegment'.
addResponseToSegment :: Response -> XRaySegment -> XRaySegment
addResponseToSegment response =
xraySegmentHttp
. non xraySegmentHttpDef
. xraySegmentHttpResponse
. non xraySegmentHttpResponseDef
%~ (xraySegmentHttpResponseStatus ?~ statusCode (responseStatus response))
-- | We use the WAI 'V.Vault' to store data needed during traces.
data XRayVaultData
= XRayVaultData
{ xrayVaultDataTraceIdHeaderData :: !XRayTraceIdHeaderData
-- ^ Data about the current trace.
, xrayVaultDataClientConfig :: !XRayClientConfig
-- ^ Client configuration passed into the middleware.
, xrayVaultDataRootSegmentId :: !XRaySegmentId
-- ^ Segment ID of the root segment for this request.
, xrayVaultDataStdGen :: !(IORef StdGen)
-- ^ 'StdGen' for generating segment IDs and trace IDs.
, xrayVaultDataSubsegments :: !(IORef (Seq XRaySegment))
-- ^ Current list of subsegments.
}
-- | This is a 'V.Key' for the @vault@ inside each WAI 'Request'. It is used to
-- get to the 'XRayVaultData' for the current request.
xrayWaiVaultKey :: V.Key XRayVaultData
xrayWaiVaultKey = unsafePerformIO V.newKey
{-# NOINLINE xrayWaiVaultKey #-}
-- | Try to get 'XRayVaultData' from the WAI 'Request' vault.
vaultDataFromRequest :: Request -> Maybe XRayVaultData
vaultDataFromRequest = V.lookup xrayWaiVaultKey . vault
-- | Time a 'MonadIO' action and add it to the list of subsegments.
traceXRaySubsegment
:: MonadUnliftIO m
=> Request
-> Text
-> (XRaySegment -> XRaySegment)
-> m a
-> m a
traceXRaySubsegment request subsegmentName modifySubsegment action =
case V.lookup xrayWaiVaultKey (vault request) of
Nothing -> action
Just v -> traceXRaySubsegment' v subsegmentName modifySubsegment action
traceXRaySubsegment'
:: MonadUnliftIO m
=> XRayVaultData
-> Text
-> (XRaySegment -> XRaySegment)
-> m a
-> m a
traceXRaySubsegment' vaultData subsegmentName modifySubsegment action = do
-- Run action with timing
startTime <- liftIO getPOSIXTime
-- Catch any exceptions and rethrow them once we've sent the segment
finally action
$ liftIO
$ do
endTime <- getPOSIXTime
-- Generate a segment ID
segmentId <- withRandomGenIORef
(xrayVaultDataStdGen vaultData)
generateXRaySegmentId
-- Create the subsegment value and append to the subsegments.
let
subsegment =
xraySubsegment subsegmentName segmentId startTime (Just endTime)
subsegment' = modifySubsegment subsegment
atomicallyAddVaultDataSubsegment vaultData subsegment'
-- | Add subsegment to XRay vault data 'IORef'.
atomicallyAddVaultDataSubsegment :: XRayVaultData -> XRaySegment -> IO ()
atomicallyAddVaultDataSubsegment vaultData subsegment =
atomicModifyIORef' (xrayVaultDataSubsegments vaultData)
$ \subsegments -> (subsegments |> subsegment, ())
-- | Uses the trace ID and segment ID of the root segment from the vault to
-- make a subsegment independent. This is useful so nested components that
-- create subsegments don't need all of this information threaded down to them.
-- We can just decorate all of the subsegments with it before sending them off.
makeSubsegmentIndependent :: XRayVaultData -> XRaySegment -> XRaySegment
makeSubsegmentIndependent XRayVaultData {..} subsegment =
subsegment
& xraySegmentTraceId
?~ xrayTraceIdHeaderDataRootTraceId xrayVaultDataTraceIdHeaderData
& xraySegmentParentId
?~ xrayVaultDataRootSegmentId
& xraySegmentType
?~ "subsegment"