packages feed

opentracing-zipkin-v1-0.1.0.0: src/OpenTracing/Zipkin/V1/Thrift.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE TupleSections     #-}
{-# LANGUAGE ViewPatterns      #-}

{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}

module OpenTracing.Zipkin.V1.Thrift
    ( toThriftSpan

    , thriftEncodeSpan
    , thriftEncodeSpans
    )
where

import           Control.Lens
import           Data.Bifunctor
import           Data.Bits
import           Data.ByteString.Builder
import qualified Data.ByteString.Lazy     as Lazy
import           Data.ByteString.Lens
import           Data.Foldable            (foldl', toList)
import qualified Data.HashMap.Strict      as HashMap
import           Data.Int
import qualified Data.IP                  as IP
import           Data.List.NonEmpty       (NonEmpty (..))
import           Data.Semigroup           ((<>))
import           Data.Text.Lazy.Encoding  (decodeUtf8, encodeUtf8)
import qualified Data.Vector              as Vector
import           OpenTracing.Log
import           OpenTracing.Span
import           OpenTracing.Tags
import           OpenTracing.Time
import           OpenTracing.Types
import           OpenTracing.Zipkin.Types (Endpoint (..))
import qualified Thrift
import           Thrift.Protocol.Binary
import           Thrift.Transport.Empty
import           Thrift.Types
import qualified ZipkinCore_Consts        as Thrift
import           ZipkinCore_Types
    ( Annotation (..)
    , BinaryAnnotation (..)
    , Span (..)
    , endpoint_ipv4
    , endpoint_ipv6
    , endpoint_port
    , endpoint_service_name
    )
import qualified ZipkinCore_Types         as Thrift


toThriftSpan
    :: Endpoint
    -> LogFieldsFormatter
    -> FinishedSpan
    -> Thrift.Span
toThriftSpan (toThriftEndpoint -> loc) logfmt s = Thrift.Span
    { span_trace_id           = view (spanContext . to traceIdLo') s
    , span_trace_id_high      = view (spanContext . to traceIdHi') s
    , span_name               = view (spanOperation . lazy) s
    , span_id                 = view (spanContext . to ctxSpanID') s
    , span_parent_id          = view (spanContext . to ctxParentSpanID') s
    , span_annotations        = annotations
    , span_binary_annotations = binaryAnnotations
    , span_debug              = Nothing
    , span_timestamp          = Just tstart
    , span_duration           = view (spanDuration . to micros . re _Just) s
    }
  where
    tstart = view (spanStart . to micros) s

    (annotations, binaryAnnotations)
        = bimap Vector.fromList Vector.fromList
        . first (<> annFromLogs (view spanLogs s))
        $ annFromTags (view spanTags s)

    annFromTags :: Tags -> ([Thrift.Annotation], [Thrift.BinaryAnnotation])
    annFromTags = perhapsLocal . foldl' go ([],[]) . HashMap.toList . fromTags
      where
        go acc (SpanKind sk) =
            let ann = Thrift.Annotation
                    { annotation_timestamp = tstart
                    , annotation_host      = Just loc
                    , annotation_value     = case sk of
                          RPCClient -> Thrift.cLIENT_SEND
                          RPCServer -> Thrift.sERVER_RECV
                          Producer  -> Thrift.mESSAGE_SEND
                          Consumer  -> Thrift.mESSAGE_RECV
                    }
             in first (ann:) acc

        go acc (k,v) =
            let (anntyp, annval) = toThriftTag v
                ann              = Thrift.BinaryAnnotation
                    { binaryAnnotation_key             = view lazy k
                    , binaryAnnotation_value           = annval
                    , binaryAnnotation_annotation_type = anntyp
                    , binaryAnnotation_host            = Just loc
                    }
             in second (ann:) acc

        -- if we don't have a 'SpanKind', we're supposed to tell Zipkin about us
        -- via a 'BinaryAnnotation'
        perhapsLocal ([],bs) = ([],) . (:bs) $ Thrift.BinaryAnnotation
            { binaryAnnotation_key             = Thrift.lOCAL_COMPONENT
            , binaryAnnotation_value           = encodeUtf8 $ endpoint_service_name loc
            , binaryAnnotation_annotation_type = Thrift.STRING
            , binaryAnnotation_host            = Just loc
            }
        perhapsLocal xs = xs

    annFromLogs :: [LogRecord] -> [Thrift.Annotation]
    annFromLogs = foldl' go []
      where
        go acc (LogRecord t fs) = Thrift.Annotation
            { annotation_timestamp = micros t
            , annotation_host      = Just loc
            , annotation_value     = case fs of
                  (Event x :| []) -> view lazy x -- proper zipkin annotation
                  fields          -> decodeUtf8 . toLazyByteString $ logfmt fields
            }
            : acc

thriftEncodeSpan :: Thrift.Span -> Lazy.ByteString
thriftEncodeSpan = Thrift.encode_Span (BinaryProtocol EmptyTransport)

thriftEncodeSpans :: Traversable t => t Thrift.Span -> Lazy.ByteString
thriftEncodeSpans
    = thriftEncodeVal
    . TList (T_STRUCT Thrift.typemap_Span)
    . toList
    . fmap Thrift.from_Span

thriftEncodeVal :: ThriftVal -> Lazy.ByteString
thriftEncodeVal = Thrift.serializeVal (BinaryProtocol EmptyTransport)

toThriftTag :: TagVal -> (Thrift.AnnotationType, Lazy.ByteString)
toThriftTag (BoolT   v) = (Thrift.BOOL, if v then "1" else "0")
toThriftTag (StringT v) = (Thrift.STRING, view (lazy . to encodeUtf8) v)
toThriftTag (IntT    v) = (Thrift.I64, toLazyByteString . int64BE $ v)
toThriftTag (DoubleT v) = (Thrift.DOUBLE, toLazyByteString . doubleBE $ v)
toThriftTag (BinaryT v) = (Thrift.BYTES, v)

toThriftEndpoint :: Endpoint -> Thrift.Endpoint
toThriftEndpoint Endpoint{..} = Thrift.Endpoint
    { endpoint_ipv4         = packIPv4 $ fromIPv4 ipv4
    , endpoint_port         = maybe 0 (fromIntegral . fromPort) port
    , endpoint_service_name = view lazy serviceName
    , endpoint_ipv6         = packIPv6 . fromIPv6 <$> ipv6
    }
  where
    packIPv4 :: IP.IPv4 -> Int32
    packIPv4 ip =
        let [a,b,c,d] = IP.fromIPv4 ip
         in fromIntegral $ a `shiftL` 24 .|. b `shiftL` 16 .|. c `shiftL` 8 .|. d

    packIPv6 :: IP.IPv6 -> Lazy.ByteString
    packIPv6 = view packedBytes . map fromIntegral . IP.fromIPv6b


traceIdLo' :: SpanContext -> Int64
traceIdLo' = fromIntegral . traceIdLo . ctxTraceID

traceIdHi' :: SpanContext -> Maybe Int64
traceIdHi' = fmap fromIntegral . traceIdHi . ctxTraceID

ctxSpanID' :: SpanContext -> Int64
ctxSpanID' = fromIntegral . ctxSpanID

ctxParentSpanID' :: SpanContext -> Maybe Int64
ctxParentSpanID' = fmap fromIntegral . ctxParentSpanID