packages feed

servant-tracing-0.2.0.0: src/Tracing/DataDog.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Tracing.DataDog (
    publishDataDog,
    DataDogSpan(..)
    ) where

import Tracing.Core (Span(..), SpanId(..), OpName(..), TraceId(..), SpanContext(..),
    SpanRelation(..), SpanTag(..))

import Control.Monad.Trans (liftIO, MonadIO)
import Control.Monad (void)
import Data.Monoid ((<>), mempty)
import Data.Aeson
import Data.Maybe (fromMaybe)
import Data.Int (Int64)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy.Char8 as LBSC8
import qualified Data.ByteString.Lex.Integral as BS
import Network.HTTP.Client
import Network.HTTP.Types.Header (Header)
import Data.Aeson.Types (ToJSON(toJSON))
import qualified Data.Map as Map


-- | Publish 'Span' in the <https://docs.datadoghq.com/api/?lang=bash#send-traces DataDog format> . No call is made
-- on an empty span list
publishDataDog :: MonadIO m =>
    String -- ^ The address of the backend server
    -> Manager
    -> [Header]
    -> [Span] -- ^ The traced spans to send to a DataDog backend
    -> m (Maybe (Response T.Text))
publishDataDog _ _ _ [] = pure Nothing
publishDataDog destination manager additionalHeaders spans =
    liftIO . fmap (Just . fmap decode) $ httpLbs ddReq manager
    where
        decode = T.decodeUtf8 . LBS.toStrict
        req = parseRequest_ destination
        body = RequestBodyLBS . encode $ DataDogSpan <$> spans
        ddReq = req { method = "POST",
                      requestBody = body,
                      requestHeaders = [("content-type", "application/json")] <> additionalHeaders
                    }

newtype DataDogSpan = DataDogSpan Span
instance ToJSON DataDogSpan where
    toJSON (DataDogSpan span) = object $ [
        "trace_id"  .= (unTrace . traceId $ context span),
        "span_id" .= (unSpan . spanId $ context span),
        "name" .=  unOp (operationName span),
        "resource" .= unOp (operationName span),
        "start" .= (floor . toNanos $ timestamp span :: Int64),
        "type" .= ("web"::T.Text),
        "duration" .= (toNanos $ duration span),
        "service" .= (serviceName span),
        "meta" .= (unTag <$> tags span)
        ] <>
        parentId (relations span)
        where
            toNanos = (*) 1000000000
            unOp (OpName n) = n
            unSpan (SpanId sid) = sid
            unTrace (TraceId tid) = tid
            parentId (ChildOf ctx:_) = ["parent_id" .= (unSpan $ spanId ctx)]
            parentId (FollowsFrom ctx:_) = ["parent_id" .= (unSpan $ spanId ctx)]
            parentId _ = []
            padLeft 0 txt = txt
            padLeft n txt
                | T.length txt < n = padLeft n ("0"<>txt)
                | otherwise = txt
            unTag (TagString a) = toJSON a
            unTag (TagBool a) = toJSON a
            unTag (TagInt a) = toJSON a
            unTag (TagDouble a) = toJSON a