packages feed

opentelemetry-lightstep-0.0.0.0: src/OpenTelemetry/LightStep/Exporter.hs

{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.LightStep.Exporter where

import Control.Concurrent.MVar
import Control.Lens
import Control.Monad.Catch
import Control.Monad.IO.Class
import Data.ProtoLens.Message (defMessage)
import qualified Data.Text as T
import Data.Version (showVersion)
import Network.GRPC.Client
import Network.GRPC.Client.Helpers
import Network.GRPC.HTTP2.ProtoLens
import Network.HTTP2.Client
import OpenTelemetry.Common
import OpenTelemetry.LightStep.Config
import Paths_opentelemetry_lightstep (version)
import qualified Proto.Collector as P
import Proto.Collector_Fields hiding (spanContext, spanId)
import qualified Proto.Collector_Fields as P
import Proto.Google.Protobuf.Timestamp_Fields
import System.Timeout

data LightStepClient
  = LightStepClient
      { lscGrpcVar :: MVar GrpcClient,
        lscReporter :: P.Reporter,
        lscConfig :: LightStepConfig
      }

d_ :: String -> IO ()
d_ = putStrLn

createLightStepExporter :: LightStepConfig -> IO (Exporter Span)
createLightStepExporter cfg = do
  client <- mkClient cfg
  pure
    $! Exporter
      ( \sps -> do
          reportSpans client sps
          pure ExportSuccess
      )
      ( do
          _ <- closeClient client
          pure ()
      )

mkClient :: LightStepConfig -> IO LightStepClient
mkClient lscConfig@(LightStepConfig {..}) = do
  grpc <- makeGrpcClient lscConfig
  lscGrpcVar <- newMVar grpc
  let lscReporter =
        defMessage
          & reporterId .~ 2
          & tags
            .~ ( [ defMessage & key .~ "lightstep.component_name" & stringValue .~ lsServiceName,
                   defMessage & key .~ "lightstep.tracer_platform" & stringValue .~ "haskell",
                   defMessage & key .~ "lightstep.tracer_version" & stringValue .~ (T.pack $ showVersion version)
                 ]
                   <> [ defMessage & key .~ k & stringValue .~ v
                        | (k, v) <- lsGlobalTags
                      ]
               )
  pure LightStepClient {..}

convertSpan :: Span -> P.Span
convertSpan s@(Span {..}) =
  defMessage
    & operationName .~ spanOperation
    & startTimestamp
      .~ ( defMessage
             & seconds .~ fromIntegral (spanStartedAt `div` 1_000_000_000)
             & nanos .~ fromIntegral (rem spanStartedAt 1_000_000_000)
         )
    & P.spanContext
      .~ ( defMessage
             & traceId .~ tid
             & P.spanId .~ sid
         )
  where
    TId tid = (spanTraceId s)
    SId sid = spanId s

closeClient :: LightStepClient -> IO (Either ClientError ())
closeClient LightStepClient {..} = readMVar lscGrpcVar >>= runExceptT . close

reportSpans :: LightStepClient -> [Span] -> IO ()
reportSpans client@(LightStepClient {..}) (map convertSpan -> sps) = do
  let LightStepConfig {..} = lscConfig
  let tryOnce = do
        let req ::
              IO
                ( Maybe
                    ( Either
                        ClientError
                        (Either TooMuchConcurrency (RawReply P.ReportResponse))
                    )
                )
            req =
              timeout 3_000_000 $ do
                grpc <- readMVar lscGrpcVar
                runExceptT $ do
                  rawUnary
                    (RPC :: RPC P.CollectorService "report")
                    grpc
                    ( defMessage
                        & auth .~ (defMessage & accessToken .~ lsToken)
                        & spans .~ sps
                        & reporter .~ lscReporter
                    )
        fst
          <$> generalBracket
            (pure ())
            ( \_ -> \case
                ExitCaseException err -> do
                  d_ $ "reportSpans failed: " <> show (err :: SomeException)
                ExitCaseAbort -> do
                  d_ $ "reportSpans aborted"
                ExitCaseSuccess _ -> pure ()
            )
            (\_ -> req)
  ret <- tryOnce
  ret2 <- case ret of
    Nothing -> do
      d_ "GRPC client is stuck, trying to reconnect"
      reconnectClient client
      -- one retry after reconnect
      tryOnce
    _ -> pure ret
  case ret2 of
    Nothing -> pure ()
    _ -> pure ()
  -- d_ $ show ret2
  pure ()

reconnectClient :: LightStepClient -> IO ()
reconnectClient LightStepClient {..} = do
  d_ "reconnectClient begin"
  -- inc 1 reconnectCountVar
  newGrpc <- makeGrpcClient lscConfig
  oldGrpc <- swapMVar lscGrpcVar newGrpc
  _ <- runExceptT $ close oldGrpc
  d_ "reconnectClient end"

makeGrpcClient :: LightStepConfig -> IO GrpcClient
makeGrpcClient LightStepConfig {..} = do
  newGrpcOrError <-
    runExceptT $
      setupGrpcClient
        ( (grpcClientConfigSimple lsHostName lsPort True)
            { _grpcClientConfigCompression = compression,
              _grpcClientConfigTimeout = Timeout 5, -- seconds
              _grpcClientConfigGoAwayHandler = \_ -> liftIO $ putStrLn "GoAway handler fired"
            }
        )
  case newGrpcOrError of
    Right newGrpc -> pure newGrpc
    Left err -> throwM err
  where
    compression = if False then gzip else uncompressed