packages feed

eventlog-live-otelcol-0.6.0.0: src/GHC/Eventlog/Live/Otelcol/Exporter/Logs.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module GHC.Eventlog.Live.Otelcol.Exporter.Logs (
  -- * Logs
  ExportLogsResult (..),
  RejectedLogsError (..),
  exportResourceLogs,
) where

import Control.Exception (Exception (..), SomeException (..), catch)
import Control.Monad (unless)
import Control.Monad.IO.Class (MonadIO (..))
import Data.Int (Int64)
import Data.Machine (ProcessT, await, construct, yield)
import Data.Semigroup (Sum (..))
import Data.Text (Text)
import Data.Vector qualified as V
import GHC.Eventlog.Live.Machine.Core (Tick (..))
import Lens.Family2 ((^.))
import Network.GRPC.Client qualified as G
import Network.GRPC.Client.StreamType.IO qualified as G
import Network.GRPC.Common qualified as G
import Network.GRPC.Common.Protobuf (Protobuf)
import Network.GRPC.Common.Protobuf qualified as G
import Proto.Opentelemetry.Proto.Collector.Logs.V1.LogsService qualified as OLS
import Proto.Opentelemetry.Proto.Collector.Logs.V1.LogsService_Fields qualified as OLS
import Proto.Opentelemetry.Proto.Logs.V1.Logs qualified as OL
import Proto.Opentelemetry.Proto.Logs.V1.Logs_Fields qualified as OL
import Text.Printf (printf)

--------------------------------------------------------------------------------
-- OpenTelemetry gRPC Exporters
--------------------------------------------------------------------------------

--------------------------------------------------------------------------------
-- OpenTelemetry Exporter Result for Logs

data ExportLogsResult
  = ExportLogsResult
  { exportedLogRecords :: !Int64
  , rejectedLogRecords :: !Int64
  , maybeSomeException :: Maybe SomeException
  }
  deriving (Show)

pattern ExportLogsSuccess :: Int64 -> ExportLogsResult
pattern ExportLogsSuccess exportedLogRecords =
  ExportLogsResult exportedLogRecords 0 Nothing

pattern ExportLogsError :: Int64 -> Int64 -> SomeException -> ExportLogsResult
pattern ExportLogsError exportedLogRecords rejectedLogRecords someException =
  ExportLogsResult exportedLogRecords rejectedLogRecords (Just someException)

data RejectedLogsError
  = RejectedLogsError
  { rejectedLogRecords :: !Int64
  , errorMessage :: !Text
  }
  deriving (Show)

instance Exception RejectedLogsError where
  displayException :: RejectedLogsError -> String
  displayException RejectedLogsError{..} =
    printf "Error: OpenTelemetry Collector rejected %d log records with message: %s" rejectedLogRecords errorMessage

--------------------------------------------------------------------------------
-- OpenTelemetry gRPC Exporter for Logs

exportResourceLogs ::
  G.Connection ->
  ProcessT IO (Tick OLS.ExportLogsServiceRequest) (Tick ExportLogsResult)
exportResourceLogs conn = construct $ go False
 where
  go exportedResourceLogs =
    await >>= \case
      Tick -> do
        unless exportedResourceLogs $
          yield (Item $ ExportLogsSuccess 0)
        yield Tick
        go False
      Item exportLogsServiceRequest -> do
        exportLogsResult <- liftIO (sendResourceLogs exportLogsServiceRequest)
        yield (Item exportLogsResult)
        go True

  sendResourceLogs :: OLS.ExportLogsServiceRequest -> IO ExportLogsResult
  sendResourceLogs exportLogsServiceRequest =
    doGrpc `catch` handleSomeException
   where
    !exportedLogRecords = countLogRecordsInExportLogsServiceRequest exportLogsServiceRequest

    doGrpc :: IO ExportLogsResult
    doGrpc = do
      G.nonStreaming conn (G.rpc @(Protobuf OLS.LogsService "export")) (G.Proto exportLogsServiceRequest) >>= \case
        G.Proto resp
          | resp ^. OLS.partialSuccess . OLS.rejectedLogRecords == 0 -> do
              pure $ ExportLogsSuccess exportedLogRecords
          | otherwise -> do
              let !rejectedLogRecords = resp ^. OLS.partialSuccess . OLS.rejectedLogRecords
              let !rejectedLogsError = RejectedLogsError{errorMessage = resp ^. OLS.partialSuccess . OLS.errorMessage, ..}
              pure $ ExportLogsError exportedLogRecords rejectedLogRecords (SomeException rejectedLogsError)

    handleSomeException :: SomeException -> IO ExportLogsResult
    handleSomeException someException = pure $ ExportLogsError 0 exportedLogRecords someException

type instance G.RequestMetadata (Protobuf OLS.LogsService meth) = G.NoMetadata
type instance G.ResponseInitialMetadata (Protobuf OLS.LogsService meth) = G.NoMetadata
type instance G.ResponseTrailingMetadata (Protobuf OLS.LogsService meth) = G.NoMetadata

--------------------------------------------------------------------------------
-- Internal Helpers
--------------------------------------------------------------------------------

{- |
Internal helper.
Count the number of `OL.NumberDataPoint` values in an `OLS.ExportLogsServiceRequest`.
-}
{-# SPECIALIZE countLogRecordsInExportLogsServiceRequest :: OLS.ExportLogsServiceRequest -> Int64 #-}
{-# SPECIALIZE countLogRecordsInExportLogsServiceRequest :: OLS.ExportLogsServiceRequest -> Word #-}
countLogRecordsInExportLogsServiceRequest :: (Integral i) => OLS.ExportLogsServiceRequest -> i
countLogRecordsInExportLogsServiceRequest exportLogsServiceRequest =
  getSum $ foldMap (Sum . countLogRecordsInResourceLogs) (exportLogsServiceRequest ^. OLS.vec'resourceLogs)

{- |
Internal helper.
Count the number of `OL.NumberDataPoint` values in an `OL.ResourceLogs`.
-}
{-# SPECIALIZE countLogRecordsInResourceLogs :: OL.ResourceLogs -> Int64 #-}
{-# SPECIALIZE countLogRecordsInResourceLogs :: OL.ResourceLogs -> Word #-}
countLogRecordsInResourceLogs :: (Integral i) => OL.ResourceLogs -> i
countLogRecordsInResourceLogs resourceLogs =
  getSum $ foldMap (Sum . countLogRecordsInScopeLogs) (resourceLogs ^. OL.vec'scopeLogs)

{- |
Internal helper.
Count the number of `OL.NumberDataPoint` values in an `OL.ScopeLogs`.
-}
{-# SPECIALIZE countLogRecordsInScopeLogs :: OL.ScopeLogs -> Int64 #-}
{-# SPECIALIZE countLogRecordsInScopeLogs :: OL.ScopeLogs -> Word #-}
countLogRecordsInScopeLogs :: (Integral i) => OL.ScopeLogs -> i
countLogRecordsInScopeLogs scopeLogs =
  fromIntegral $
    V.length (scopeLogs ^. OL.vec'logRecords)