aws-xray-client-0.1.0.0: library/Network/AWS/XRayClient/TraceId.hs
{-# LANGUAGE FlexibleContexts #-}
module Network.AWS.XRayClient.TraceId
( amazonTraceIdHeaderName
, -- * Trace ID
XRayTraceId(..)
, generateXRayTraceId
, makeXRayTraceId
, XRaySegmentId(..)
, generateXRaySegmentId
-- * Trace ID Header
, XRayTraceIdHeaderData(..)
, xrayTraceIdHeaderData
, parseXRayTraceIdHeaderData
, makeXRayTraceIdHeaderValue
) where
import Prelude
import Control.DeepSeq (NFData)
import Data.Aeson
import Data.Bifunctor (first)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import Data.Char (intToDigit)
import Data.IORef
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time.Clock.POSIX (getPOSIXTime)
import GHC.Generics
import Network.HTTP.Types.Header
import Numeric (showHex)
import System.Random
import System.Random.XRayCustom
-- | Variable for "X-Amzn-Trace-Id" so you don't have to worry about
-- misspelling it.
amazonTraceIdHeaderName :: HeaderName
amazonTraceIdHeaderName = "X-Amzn-Trace-Id"
-- | A trace_id consists of three numbers separated by hyphens. For example,
-- 1-58406520-a006649127e371903a2de979. This includes: The version number, that
-- is, 1. The time of the original request, in Unix epoch time, in 8
-- hexadecimal digits. For example, 10:00AM December 2nd, 2016 PST in epoch
-- time is 1480615200 seconds, or 58406520 in hexadecimal. A 96-bit identifier
-- for the trace, globally unique, in 24 hexadecimal digits.
newtype XRayTraceId = XRayTraceId { unXRayTraceId :: Text }
deriving (Show, Eq)
deriving newtype (FromJSON, ToJSON, NFData)
-- TODO: Make a parse function and don't export constructor
-- parseXRayTraceId :: (MonadError String m) => ByteString -> m XRayTraceId
-- | Generates an 'XRayTraceId' in 'IO'. WARNING: This uses the global
-- 'StdGen', so this can be a bottleneck in multi-threaded applications.
generateXRayTraceId :: IORef StdGen -> IO XRayTraceId
generateXRayTraceId ioRef = do
timeInSeconds <- round <$> getPOSIXTime
withRandomGenIORef ioRef $ makeXRayTraceId timeInSeconds
makeXRayTraceId :: Int -> StdGen -> (XRayTraceId, StdGen)
makeXRayTraceId timeInSeconds gen = first make $ randomHexString 24 gen
where
make hexString =
XRayTraceId $ T.pack $ "1-" ++ showHex timeInSeconds "" ++ "-" ++ hexString
-- | Generates a random hexadecimal string of a given length.
randomHexString :: Int -> StdGen -> (String, StdGen)
randomHexString n gen =
replicateRandom n gen $ first intToDigit . randomR (0, 15)
-- | A 64-bit identifier for the segment, unique among segments in the same
-- trace, in 16 hexadecimal digits.
newtype XRaySegmentId = XRaySegmentId { unXRaySegmentId :: Text }
deriving (Show, Eq)
deriving newtype (FromJSON, ToJSON, NFData)
-- TODO: Make parse function for XRaySegmentId so it is safer.
-- | Generates an 'XRaySegmentId' using a given 'StdGen'.
generateXRaySegmentId :: StdGen -> (XRaySegmentId, StdGen)
generateXRaySegmentId = first (XRaySegmentId . T.pack) . randomHexString 16
-- | This holds the data from the X-Amzn-Trace-Id header. See
-- http://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader
data XRayTraceIdHeaderData
= XRayTraceIdHeaderData
{ xrayTraceIdHeaderDataRootTraceId :: !XRayTraceId
, xrayTraceIdHeaderDataParentId :: !(Maybe XRaySegmentId)
, xrayTraceIdHeaderDataSampled :: !(Maybe Bool)
} deriving (Show, Eq, Generic)
-- | Constructor for 'XRayTraceIdHeaderData'.
xrayTraceIdHeaderData :: XRayTraceId -> XRayTraceIdHeaderData
xrayTraceIdHeaderData traceId = XRayTraceIdHeaderData
{ xrayTraceIdHeaderDataRootTraceId = traceId
, xrayTraceIdHeaderDataParentId = Nothing
, xrayTraceIdHeaderDataSampled = Nothing
}
-- | Try to parse the value of the X-Amzn-Trace-Id into a
-- 'XRayTraceIdHeaderData'.
parseXRayTraceIdHeaderData :: ByteString -> Maybe XRayTraceIdHeaderData
parseXRayTraceIdHeaderData rawHeader = do
components <- traverse parseHeaderComponent $ BS8.split ';' rawHeader
traceId <- lookup "Root" components
pure XRayTraceIdHeaderData
{ xrayTraceIdHeaderDataRootTraceId = XRayTraceId (T.decodeUtf8 traceId)
, xrayTraceIdHeaderDataParentId = XRaySegmentId
. T.decodeUtf8
<$> lookup "Parent" components
, xrayTraceIdHeaderDataSampled = lookup "Sampled" components >>= readSampled
}
where
readSampled :: ByteString -> Maybe Bool
readSampled "0" = Just False
readSampled "1" = Just True
readSampled _ = Nothing
-- | Turns a 'XRayTraceIdHeaderData' into a 'ByteString' meant for the
-- X-Amzn-Trace-Id header value.
makeXRayTraceIdHeaderValue :: XRayTraceIdHeaderData -> ByteString
makeXRayTraceIdHeaderValue XRayTraceIdHeaderData {..} =
traceIdPart <> parentPart <> sampledPart
where
traceIdPart =
"Root=" <> T.encodeUtf8 (unXRayTraceId xrayTraceIdHeaderDataRootTraceId)
parentPart = maybe
""
((";Parent=" <>) . T.encodeUtf8 . unXRaySegmentId)
xrayTraceIdHeaderDataParentId
sampledPart = maybe
""
((";Sampled=" <>) . BS8.pack . show . fromEnum)
xrayTraceIdHeaderDataSampled
-- | Header components look like Name=Value
parseHeaderComponent :: ByteString -> Maybe (ByteString, ByteString)
parseHeaderComponent rawComponent = case BS8.split '=' rawComponent of
[name, value] -> Just (name, value)
_ -> Nothing